Last updated: 2026-10-26

U
Undergraduate level

Object Orientation as the Beginning of Software Engineering

The moment one object collaborates with another, design choices start to affect how easily the resulting program can be tested, changed, understood and trusted. A private field can introduce an invariant. An interface can introduce a contract. A unit test can become evidence about behaviour. None of that requires a separate module โ€” it is already sitting inside the Java you are learning to write.

This does not mean a first object-oriented programming assessment has to teach the whole of software engineering. The module's primary purpose stays exactly what it says: learning to build correct, well-structured Java classes. But small OOP decisions keep opening onto a wider discipline, and this page โ€” and the three it links to โ€” exist to make that visible without turning every exercise into a checklist.

Note well. Nothing below is a hidden requirement. Some of it is directly taught and marked. The rest is signalled: mentioned so you can recognise it, investigate it, and apply it only where it genuinely improves your work.

Five Ways an Idea Can Sit in This Module

Every software-engineering idea on these pages sits at one of five points. Knowing which one saves you from two opposite mistakes: ignoring something that was actually taught, and treating a passing mention as a secret marking requirement.

StageWhat it means
Directly taughtExplained, demonstrated, and practised with support. You are expected to use it correctly.
SignalledMentioned, alluded to, or shown in an example as a potentially useful direction โ€” not yet taught in full.
Independently exploredInvestigated by you, through documentation, experimentation, or other credible sources.
Purposefully appliedUsed because it makes the solution better, not because you suspect a marker is looking for it.
Eventually expectedBecomes part of competent practice in later assessments, once it has had time to be taught properly.

Nothing on this page claims to have been fully taught just because it is mentioned. Where a signal appears, it says so, and says what you are invited to do with it.

Three Pages This One Opens Onto

This page stays with the class itself: the object, its invariants, and the small set of collaborating classes around it. Three shorter pages pick up what happens once that object sits inside a real, changing, shared codebase:

  • Testing Object Behaviour โ€” writing JUnit tests alongside the class they describe, and what a test is actually evidence of.
  • Objects in a Codebase โ€” Git history, documentation, and the privacy, security, and accessibility questions a class's design decisions raise.
  • Independent Learning and AI โ€” turning programming work into evidence of learning, and treating an AI tool as something to inspect rather than obey.

A Small Running Example Applied / MethodologicalKnowledge with a 5โ€“10 year half-life โ€” stable practice

Every signal below reuses one small domain: a university equipment-loan system. It stays small enough to read in one sitting, but it raises real questions about availability, overdue loans, personal data, and what happens when the rules change โ€” which is exactly what makes it useful for noticing adjacent practices rather than inventing a reason to need them.

Start with a single class, doing one thing, protecting one rule: an item is either available or on loan to exactly one borrower.

public final class EquipmentItem {
    private final String assetId;
    private final String description;
    private Borrower currentBorrower;

    public EquipmentItem(String assetId, String description) {
        if (assetId == null || assetId.isBlank()) {
            throw new IllegalArgumentException("Asset ID is required");
        }
        if (description == null || description.isBlank()) {
            throw new IllegalArgumentException("Description is required");
        }
        this.assetId = assetId;
        this.description = description;
    }

    public boolean isAvailable() {
        return currentBorrower == null;
    }

    public void lendTo(Borrower borrower) {
        if (!isAvailable()) {
            throw new IllegalStateException("Item is already on loan");
        }
        if (borrower == null) {
            throw new IllegalArgumentException("Borrower is required");
        }
        currentBorrower = borrower;
    }

    public void returnItem() {
        currentBorrower = null;
    }
}

Read it as plain OOP first, because that is what it is: private state nobody outside the class can reach directly; a constructor that refuses to build an invalid object; methods that are the only way to change that state; and a type, Borrower, standing in for a second collaborating class. isAvailable() is derived, not stored โ€” there is no separate boolean onLoan field sitting next to currentBorrower, which matters more than it looks: two fields that both describe the same fact can disagree with each other, and one that is computed from the other cannot.

Encapsulation Signals Invariants FoundationalKnowledge that endures for decades โ€” core principles

private does not make a class well designed by itself. Encapsulation starts to matter once the public methods make invalid states difficult or impossible to create โ€” the field being hidden is the mechanism, not the point.

A Weaker Version

public class EquipmentItem {
    public String assetId;
    public boolean onLoan;
    public String borrowerId;
}

Ask what this class permits, not what it intends. Nothing stops onLoan == true with borrowerId left null. Nothing stops a borrower being recorded while onLoan stays false. Nothing stops assetId being blank, or being changed later by code that has nothing to do with lending. Every one of those is a state the class's own author never meant to allow, and every one of them is reachable from outside the class because nothing stands in the way.

Don't do this! Public mutable fields hand every invariant to whoever calls the class โ€” including code that has never heard of the rule it is supposed to be following.

The Behavioural Version

The first version of EquipmentItem above is the fix: validate at the boundary (the constructor, lendTo), fail loudly on the way in, and every other method can assume the invariant rather than re-checking it. That assumption โ€” "a valid object never needs re-validating" โ€” is what lets the rest of a class stay simple. Parnas's classic argument for module decomposition makes the general case this is one instance of: a module's boundary should be drawn around a design decision likely to change or go wrong, hiding it from everything that does not need to know1. "Which fields are currently valid" is exactly that kind of decision.

See Advanced Java & the JVM for encapsulation from the other direction: what "invariant" means formally, and why an immutable object's invariants only need checking once.advanced java covers the jvm side of this

Independent-learning lead. Worth investigating on your own: Java records, which generate the boilerplate for an immutable, validated data holder; constructor validation generally; and factory methods as an alternative to a public constructor when construction itself needs a decision. None of these are required for a class this small โ€” they are directions, not a checklist.

A Class With Too Many Jobs Signals Cohesion FoundationalKnowledge that endures for decades โ€” core principles

The equipment system eventually needs to lend items, persist them somewhere, and notify someone when a loan is overdue. The easiest thing to write is one class that does all three:

public class LoanSystem {
    public void lendItem(String assetId, String borrowerId) { /* ... */ }
    public void returnItem(String assetId) { /* ... */ }
    public void saveToFile(String path) { /* ... */ }
    public void sendReminderEmail(String borrowerId) { /* ... */ }
    public void printReport() { /* ... */ }
}

Ask the same questions a reviewer would ask before touching it. Which responsibilities actually concern lending? Which concern storage? Which concern talking to an email system that has nothing to do with loans? If the file format changes, does the lending logic really need recompiling alongside it? And practically: how would you unit-test lendItem without a real file on disk and a real email server listening?

A class that needs "and" to describe it โ€” a LoanSystem that lends, saves, and emails โ€” is usually two or three classes wearing one name. Separating them is not decoration; it is what makes each piece independently testable and independently replaceable:

public final class LoanService {
    private final LoanRepository repository;
    private final NotificationService notifications;

    public LoanService(LoanRepository repository, NotificationService notifications) {
        this.repository = repository;
        this.notifications = notifications;
    }
}
This is the approved direction, not the only correct answer. High cohesion (a class does one thing; most of its fields are used by most of its methods) and low coupling (classes depend on narrow contracts, not each other's internals) are questions to ask of a design, not a rule with one right size of class.

Independent-learning lead. This is where the Single Responsibility Principle, cohesion, and coupling live in full โ€” see SOLID Principles. Treat SRP as a question to ask ("does this class have more than one reason to change?"), not a commandment to recite.

Interfaces Signal Contracts and Replaceable Decisions FoundationalKnowledge that endures for decades โ€” core principles

LoanService above needs somewhere to put loans, but it does not need to know how they are stored. Separate the promise from the implementation:

public interface LoanRepository {
    void save(Loan loan);
    Optional<Loan> findActiveLoanFor(String assetId);
}

public final class InMemoryLoanRepository implements LoanRepository {
    // Suitable for early development and for tests โ€” no file, no database.
}

A later, genuinely different implementation can satisfy the same contract without LoanService changing at all:

public final class SqlLoanRepository implements LoanRepository {
    // A later implementation might store loans in a database instead.
}

This is the same habit Data Structures as Behavioural Contracts identifies in Java's own collections framework: declare against the interface, and an implementation can change โ€” InMemoryLoanRepository to SqlLoanRepository, ArrayList to LinkedList โ€” without touching a caller. Parnas's criterion is the same one again: storage is "a decision likely to change," so the interface is drawn around exactly that boundary1.same interface-first habit as the collections framework

Note well. An interface is not automatically an improvement. A single tiny program may not need one for every class. The question is whether the abstraction separates a decision that is genuinely likely to vary, or helps testing and understanding โ€” not whether it is present.

Independent-learning lead. Dependency injection (how LoanService receives its LoanRepository rather than constructing one itself), and testing seams more generally, are worth following from here โ€” Test Doubles picks this up directly.

Composition Signals Architectural Choice FoundationalKnowledge that endures for decades โ€” core principles

LoanService has a LoanRepository and a NotificationService โ€” it does not extend either of them. Ask the question the design answers before writing any code: is a LoanService a kind of repository, or does it simply use one? The second is true here, and "has-a, wired at construction time" is composition, not inheritance.

See Inheritance & Composition for the full argument, including the classic Square/Rectangle failure that shows what goes wrong when "is-a" is assumed rather than checked. The short version carries over directly: deep inheritance trees are rigid, because every subclass inherits every ancestor's decisions permanently; composed objects can be rewired per use, and substituted for a test double without any class hierarchy to disturb.

Independent-learning lead. The strategy and adapter patterns are both composition applied to a specific recurring shape โ€” worth looking up once the need for one appears in your own design, not before. Naming a pattern does not improve a design; being able to say which problem it solves does. See Design Patterns.

Exceptions Signal Failure Contracts Applied / MethodologicalKnowledge with a 5โ€“10 year half-life โ€” stable practice

lendTo already throws two different exceptions for two different reasons, and the difference is the whole point: IllegalArgumentException means the caller passed something wrong; IllegalStateException means the request was reasonable but the object's current state makes it impossible right now. Ask, for any failure: is this a bad argument, or a bad moment? Should the caller recover, or is this a programming error that should crash loudly in development?

Not every failure needs an exception. A result type can make an expected, recoverable outcome part of the method's own return type instead of a thrown object:

public enum LendingResult {
    SUCCESS,
    ITEM_ALREADY_ON_LOAN,
    BORROWER_NOT_ELIGIBLE
}

Neither style is universally correct. Exceptions suit failures that are genuinely exceptional and that most callers will not handle individually; a result type or Optional suits an outcome a caller is expected to check every time. Treat "always use exceptions" and "never use exceptions" as both wrong, and ask instead what a caller of this specific method actually needs to do with a failure.

Collections Signal Algorithmic and Domain Decisions FoundationalKnowledge that endures for decades โ€” core principles

A repository needs somewhere to keep its items. The obvious first instinct is a list:

private final List<EquipmentItem> items = new ArrayList<>();

But the operation that matters most is "find the item with this asset ID" โ€” and a list answers that by scanning every element. A map states the actual shape of the problem instead:

private final Map<String, EquipmentItem> itemsByAssetId = new HashMap<>();

This is not a stylistic preference. Asset IDs are already unique by the constructor's own validation, lookup by ID is the dominant operation, and a Map is the data structure whose contract promises exactly that. See Data Structures as Behavioural Contracts for the full trade-off table (HashMap vs. TreeMap vs. a sequentially-searched list) this choice is one instance of.

Optional investigation. Comparing HashMap, TreeMap, and a list searched sequentially โ€” by behaviour first, and by measured performance if you want to go further โ€” is a reasonable thing to try. A fully rigorous benchmark is not expected at this stage; noticing the shape of the difference is the point.

Requirements Change Signals Design Quality Applied / MethodologicalKnowledge with a 5โ€“10 year half-life โ€” stable practice

The system above was built against one short requirement: an equipment item can be lent to one borrower at a time. Real requirements rarely stop there. Introduce changes one at a time and ask, each time, what actually has to move:

  1. Some items require staff approval before they can be lent.
  2. A borrower may have at most three active loans.
  3. An item may be reserved before it is collected.
  4. A borrower's name must not appear in ordinary activity logs.
  5. Status messages must be screen-reader compatible, not colour-only.

For each one: which classes change, and which stay untouched? Does isAvailable() still mean what it meant before, once "reserved" is a third state alongside available and on-loan? Were any of the earlier decisions accidentally hard-coded โ€” a single Borrower currentBorrower field, for instance, that silently assumed "at most one active loan per item" was the only limit worth enforcing, when change 2 turns out to limit the borrower instead? Which of the five changes is a new function the system must perform, and which is a quality, privacy, or accessibility requirement sitting alongside it rather than inside it? And โ€” the question tests exist to answer โ€” would your tests have told you what broke?

The point is not to build a framework that pre-empts every one of these. It is to notice that a design is partly judged by how it responds to change, not only by whether it passes today's test suite.

What Counts as Going Beyond the Immediate Task Applied / MethodologicalKnowledge with a 5โ€“10 year half-life โ€” stable practice

None of the above is rewarded for being present. A submission does not get extra credit for the number of interfaces, patterns, or frameworks it contains. A strong extension addresses a real problem in the task, is correctly applied, is tested, is explained, and has its limitations acknowledged. A weak one adds a pattern with no problem for it to solve, imports a library for something three lines of Java would do, or produces a test that cannot fail in any way that matters.

StageWhat is expected
Early Part 1Correctly apply the directly taught Java and OOP content. Relevant independent exploration may strengthen the work, but is not required.
Later Part 1Apply the taught concepts, and draw on earlier or independently acquired practices where they make the solution genuinely better.
Part 2Testing, version control, and documentation become normal, expected parts of software-engineering work, not optional extras.
Part 3 and project workIndependently select, justify, and evaluate the combination of practices appropriate to the problem in front of you.

A Worked Example: Loan Policies Applied / MethodologicalKnowledge with a 5โ€“10 year half-life โ€” stable practice

Task. Extend the equipment-loan system so that different categories of item can apply different maximum loan periods.

Directly assessed here: abstraction, encapsulation, polymorphism, and appropriate class responsibilities โ€” the taught content this task is actually testing.

Signals available, not required: parameterised tests across item categories; a Git branch for trying an alternative design before committing to one; avoiding duplicated constants for loan lengths; a requirements question about weekends and holidays; the privacy implications of an overdue notification; an honestly documented open assumption; using an AI tool to suggest edge cases and then checking each one yourself.

One defensible direction:

public interface LoanPolicy {
    LocalDate dueDateFrom(LocalDate loanDate);
}

public final class FixedDaysLoanPolicy implements LoanPolicy {
    private final int numberOfDays;

    public FixedDaysLoanPolicy(int numberOfDays) {
        if (numberOfDays < 1) {
            throw new IllegalArgumentException("Loan period must be positive");
        }
        this.numberOfDays = numberOfDays;
    }

    @Override
    public LocalDate dueDateFrom(LocalDate loanDate) {
        return loanDate.plusDays(numberOfDays);
    }
}

That is one defensible design, not the secretly required answer. An enum with behaviour attached to each constant, a plain category field read by one method, or a composed policy object could all be reasonable at this scale โ€” the right comparison is clarity, ease of extension, duplication, testability, and whether the complexity actually matches the size of the problem.

Failure Modes This Page Tries to Avoid Applied / MethodologicalKnowledge with a 5โ€“10 year half-life โ€” stable practice

  • Hidden curriculum. Nothing needed to pass should depend on spotting a subtle hint.
  • Checklist engineering. A submission does not need an interface, a pattern, a database, dependency injection, or an AI-assisted pass to be complete.
  • Premature abstraction. A three-class exercise is not an enterprise architecture, and should not be made to look like one.
  • Pattern worship. Naming a pattern is not a substitute for explaining the problem it solves.
  • Framework displacement. The examples here teach Java and object design; a framework that hides the mechanism being taught has no place in them.
  • Unverifiable independent learning. If you cannot explain and test something you say you investigated, it has not yet become part of what you actually know.
  • Testing Object Behaviour โ€” JUnit tests written alongside EquipmentItem, and what a passing test is and isn't evidence of.
  • Objects in a Codebase โ€” Git history, documentation, and the privacy, security, and accessibility questions this system's design raises.
  • Independent Learning and AI โ€” reflective practice and using an AI tool as an inspectable aid rather than an authority.
  • Inheritance & Composition โ€” the full has-a/is-a argument behind this page's composition signal.
  • Data Structures as Behavioural Contracts โ€” the collections framework this page's interface and Map/List signals both draw on.
  • SOLID Principles โ€” the Single Responsibility and dependency-inversion material behind this page's cohesion and interface signals.
  • Advanced Java & the JVM โ€” invariant-protection and immutability from underneath, including the JVM mechanics this page takes for granted.

References


  1. Parnas, D. L. (1972). On the criteria to be used in decomposing systems into modules. Communications of the ACM, 15(12), 1053โ€“1058. https://doi.org/10.1145/361598.361623 โ†ฉ โ†ฉ