- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech

Guided: Implement Object-oriented Concepts in a Java SE app
Master object-oriented programming in Java with this hands-on Guided Code Lab. You’ll build a Library Application that supports adding, lending, and returning books while practicing encapsulation, inheritance, and interfaces, and refactoring with Java records. By the end, you’ll have applied OOP concepts to a real-world app and structured code that is clean, reusable, and easy to maintain.

Lab Info
Table of Contents
-
Challenge
Introduction
In this lab, you will explore how to implement core object-oriented programming (OOP) concepts in Java by building a Library Management Application. This app will allow users to add books, lend them out, and return them. Along the way, you will use encapsulation, inheritance, and interfaces, and later simplify your data models using Java records.
Learning Objectives
- Create Java classes using proper OOP design.
- Apply encapsulation by restricting access to class fields and exposing controlled methods.
- Implement inheritance and interfaces to model roles and behaviors in the library system.
- Refactor to Java records for concise and immutable data handling. > - Each step contains tasks clearly marked with comments like
// Task 2.1
.
info> If you get stuck on a task, you can view the solution in the
solution
folder in your Filetree, or click the Task Solution link at the bottom of each task after you've attempted it. -
Challenge
Encapsulation with Core Entities
In this step, you will create the core entities of your Library system:
BookItem
to represent books andMember
to represent library members. This is where encapsulation plays a crucial role.Encapsulation means restricting direct access to an object’s internal state (fields) and instead providing controlled access through methods (getters, setters). This ensures data integrity and prevents invalid states in your application.
Why Encapsulation?
- Prevent invalid state (blank title, negative limits)
- Make objects responsible for their own validity
- Enable safe refactoring later without breaking callers ---
Library App Project Strucuture
Before making changes, you will understand the project structure and what is already provided.
The project follows a standard Java layout:
src/main/java └─ com.ps.library/ ├─ model/ # Domain classes like BookItem, MagazineItem, Member ├─ service/ # Interfaces and implementations like LibraryCatalog, LendingService └─ LibraryApp.java # Main entry point of the application
-
model: Contains the core entities of the system (BookItem, MagazineItem, Member, etc.). These represent the data and rules of the domain.
-
service: Defines and implements operations on the domain, such as catalog management (LibraryCatalog) and lending (LendingService).
-
LibraryApp.java: The main class with a simple console menu for adding books and magazines, registering members, and lending/returning items.
Now you will implement encapsulation in the
BookItem
class by making theavailable
field private. You’ve now ensured that theavailable
flag can only be modified through themarkBorrowed
andmarkReturned
methods. This controls how the variable is set and prevents unintended changes.Next, you’ll add validation to control how object fields are assigned. By placing checks in the constructor or setter methods, you ensure that values are set only when they meet the required conditions.
For the
Member
class, you will add validation during object initialization. In this step, you applied encapsulation to the core entities so their state is updated only in controlled ways.In the next step, you will see how interfaces define clear contracts for catalog and lending operations.
-
Challenge
Interfaces for Catalog and Lending
In this step, you will learn how interfaces define clear contracts for catalog and lending operations. This helps separate the definition of behavior from its implementation, making the system easier to extend or replace later.
You will work with the following files:
| Interface | Implementation | | --------------------- | ----------------------------- | | LendingService.java | SimpleLendingService.java | | LibraryCatalog.java | InMemoryLibraryCatalog.java |
Now, you will define the contract for the catalog. By adding method signatures to the
LibraryCatalog
interface, you specify what operations the catalog must support without deciding how they are implemented. Next, you will provide the implementation inInMemoryLibraryCatalog.java
. An interface can have multiple implementations, and each one can define its own behavior. For example, one using memory, another using a database, or even one using a remote service. In this step, you learned how interfaces define clear contracts that separate behavior from implementation, making the system easier to extend and maintain.In the next step, you will apply inheritance to create a hierarchy of library items, allowing shared behavior to be reused across different types.
-
Challenge
Inheritance for Item Hierarchy
In this step, you will use inheritance to capture shared state and behavior in a base type and specialize only what differs. This reduces duplication and makes the model easier to evolve. ---
Now, you will implement inheritance in
MagazineItem
. By makingMagazineItem
extendLibraryItem
, it can reuse the shared fields (likeid
andtitle
) instead of redefining them.This keeps books and magazines consistent, since
BookItem
already extendsLibraryItem
to reuse the same shared structure. With both classes inheriting from the base, their common features are handled in one place, while each class can still define its own unique details. After makingMagazineItem
inherit fromLibraryItem
, you also need to initialize the base fields (id
andtitle
). You do this inside the constructor by calling the base class constructor with thesuper
keyword. Magazines are to be read only in the library and are not part of the lending workflow. This business rule means only books can be borrowed and returned. Enforcing it here keeps lending logic consistent and prevents accidental state changes for magazines.Next, you will add checks in
SimpleLendingService
to ensure that only books can be borrowed and returned, while magazines remain restricted to in-library use. Similar to the lending, also allow only returning of Books and not Magazines. In this step, you learned how inheritance allows common fields and behavior to be shared through a base class while letting subclasses define their own details.In the next step, you will wire the application flow by connecting the UI/menu to the catalog and lending services so users can add items, register members, lend and return books.
-
Challenge
Refactor with Java Records
In this step, you will learn how a Java record can simplify immutable data modeling and reduce boilerplate.
Records
Records automatically provide final fields, a canonical constructor, accessors,
equals
,hashCode
andtoString
, so they’re a concise fit for simple bibliographic data.
Now, you will see the implementation of the
Employee
class with and without records, and observe how records simplify the code.
Without Record
In the following example, the
Employee
class is implemented as a standard class. Notice that it requires a constructor with validation, along with getter methods and atoString
implementation.public class Employee { private final String id; private final String name; private final double salary; public Employee(String id, String name, double salary) { if (id == null || id.isBlank()) throw new IllegalArgumentException("id cannot be blank"); if (name == null || name.isBlank()) throw new IllegalArgumentException("name cannot be blank"); if (salary < 0) throw new IllegalArgumentException("salary must be positive"); this.id = id; this.name = name; this.salary = salary; } public String getId() { return id; } public String getName() { return name; } public double getSalary() { return salary; } @Override public String toString() { return id + " - " + name + " ($" + salary + ")"; } }
With Record
Using a record, the same
Employee
model becomes much simpler. The record automatically generates the constructor, getters,equals
,hashCode
, andtoString
. You can still add validation inside the compact constructor if needed.public record Employee(String id, String name, double salary) { public Employee { if (id == null || id.isBlank()) throw new IllegalArgumentException("id cannot be blank"); if (name == null || name.isBlank()) throw new IllegalArgumentException("name cannot be blank"); if (salary < 0) throw new IllegalArgumentException("salary must be positive"); } }
If you don't need any validations then the object can simply be defined as:
public record Employee(String id, String name, double salary) { }
Both versions represent an employee with
id
,name
, andsalary
. The record eliminates boilerplate code while preserving immutability and allowing validation through its compact constructor. --- Now, you will change theBookInfo
class to useRecord
. Since records do not use explicit getters and setters, they provide alternate accessor methods that directly expose the variables by their names.For example the
emp.getName()
becomesemp.name()
.Now, update the
BookInfo
usage in classBookItem
so that any getters or setters are replaced with the corresponding accessor methods (title()
,author(),
andisbn()
). In this step, you learned how Java records simplify immutable data modeling by removing the need for explicit getters, setters, and boilerplate code.In the next step, you will add validation and custom exceptions to enforce domain rules.
-
Challenge
Validation and Exceptions
In this step, you will add simple validation helpers and domain-specific exceptions to enforce business rules and make failures explicit and meaningful.
Validation prevents invalid object state at construction/assignment time; domain exceptions express rule violations (e.g., item unavailable, borrow limit reached) so the UI can show clear messages.
Domain Exceptions
In real-world applications, not all errors are equal. Some represent system problems (like a null pointer), while others represent business rule violations (like a member trying to borrow too many books). Domain exceptions are custom exception classes created to represent these business-level errors.
Benefits
- Clarity: Clearly signal when a domain rule is broken.
- Better handling: Let the UI catch and show meaningful messages.
- Maintainability: Separate technical errors from business errors.
By using domain exceptions instead of generic like
IllegalStateException
orIllegalArgumentException
, your code becomes more expressive and easier to work with.
The two domain exceptions classes are defined in the package
../model/exceptions
Next you replace the generic exceptions with domain exceptions. Similarly now you will replae the generic exception with
ItemNotAvailableException
exception. --- Now you can test your application using Run button on the bottom right of the Terminal window.Alternatively, you can compile and run the application from the command line using the following commands:
Navigate to the project folder:
cd library-app
Compile the project :
mvn clean install
Run the generated jar file.
java -jar target/library-app-1.0.0.jar
You will be able to see a menu :
=== Pluralsight Library === [1] Add Book [2] Add Magazine [3] Register Member [4] List Items [5] Lend [6] Return [7] List Borrowed [8] Exit Choose:
You can test the application by typing the number of the desired option and pressing Enter. --- In this step, you used validation and domain exceptions to enforce rules and provide clear feedback when they are broken.
Congratulations! You now have a fully functional Library management application.
-
Challenge
Conclusion and Next Steps
In this lab, you applied core object-oriented concepts in building a small library application. You encapsulated state in entities, used interfaces to define contracts, applied inheritance to share behavior, wired up lending and returning flows, simplified immutable data with records, and enforced rules through validation and domain exceptions. ---
Next Steps
Now that you have built a console-based app with OOP concepts, you can:
- Extend it with a persistence layer (e.g., store books and members in a database).
- The application currently assumes there is only one copy of each book title. You can extend it to support multiple copies.
- Add a web or REST interface using Spring Boot.
- Explore advanced OOP and DDD (Domain Driven Design) concepts like value objects, aggregates, and repositories.
- Write unit tests to validate your domain rules and ensure code quality.
By practicing these, you’ll gain confidence in applying object-oriented principles to more complex and real-world Java applications.
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.