Featured resource
2025 Tech Upskilling Playbook
Tech Upskilling Playbook

Build future-ready tech teams and hit key business milestones with seven proven plays from industry leaders.

Check it out
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

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 platform
Lab Info
Level
Beginner
Last updated
Sep 24, 2025
Duration
46m

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.
Table of Contents
  1. 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.

  2. Challenge

    Encapsulation with Core Entities

    In this step, you will create the core entities of your Library system: BookItem to represent books and Member 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 the available field private. You’ve now ensured that the available flag can only be modified through the markBorrowed and markReturned 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.

  3. 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 in InMemoryLibraryCatalog.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.

  4. 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 making MagazineItem extend LibraryItem, it can reuse the shared fields (like id and title) instead of redefining them.

    This keeps books and magazines consistent, since BookItem already extends LibraryItem 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 making MagazineItem inherit from LibraryItem, you also need to initialize the base fields (id and title). You do this inside the constructor by calling the base class constructor with the super 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.

  5. 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 and toString, 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 a toString 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, and toString. 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, and salary. The record eliminates boilerplate code while preserving immutability and allowing validation through its compact constructor. --- Now, you will change the BookInfo class to use Record. 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() becomes emp.name().

    Now, update the BookInfo usage in class BookItem so that any getters or setters are replaced with the corresponding accessor methods (title(), author(), and isbn()). 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.

  6. 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 or IllegalArgumentException, 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.

  7. 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

Amar Sonwani is a software architect with more than twelve years of experience. He has worked extensively in the financial industry and has expertise in building scalable applications.

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.

Get started with Pluralsight