← SEARCHABLE ARCHIVE
Java · 2026-08-12

Java from Basics to Mastery — Part 10

Create maintainable apps with layers and tests

INTERACTIVE READING MODE →

The articles written on this blog were created with the goal of teaching. Also, the information in the article may be incorrect, so please be careful.

As a program grows, the extent to which changes spread becomes more important than the number of classes. Let's wrap up our Java series by dividing rules, storage, and screen boundaries and making behavior testable.

Hierarchy of Responsibility

public final class OrderService {
    private final ItemRepository repository;

    public OrderService(ItemRepository repository) {
        this.repository = repository;
    }

    public OrderResult buy(int itemId, int gold) {
        Item item = repository.find(itemId)
            .orElseThrow(() -> new ItemNotFoundException(itemId));

        if (gold < item.getPrice()) {
            return OrderResult.notEnoughGold();
        }

        return OrderResult.success(item, gold - item.getPrice());
    }
}

The service is responsible for purchasing rules and does not directly know the database or HTTP implementation details. The controller or screen passes input to the service and presents the result.

Putting Dependencies Outside

public interface ItemRepository {
    Optional<Item> find(int id);
}

ItemRepository repository = new FileItemRepository(path);
OrderService service = new OrderService(repository);

If the concrete storage is selected outside the constructor, tests can replace it with a memory implementation. Interfaces are not decorations for every class, but are placed at the border where external technologies change.

Test operation

@Test
void purchaseKeepsRemainingGold() {
    ItemRepository repository = new MemoryItemRepository(
        new Item(10, "Potion", 30)
    );
    OrderService service = new OrderService(repository);

    OrderResult result = service.buy(10, 100);

    assertEquals(70, result.getRemainingGold());
}

The test checks externally visible rules rather than internal method call order. Even if you clean up the implementation, you can see whether the same contract holds for the same input.

project tools

Gradle or Maven make compilation, testing, library versioning, and packaging into repeatable commands. Place build files and tests together in the repository so that the project does not run only in the development environment.

A good Java structure is not one with many hierarchies. It is a structure in which the rules for changing states, external resources, and the lifespan of concurrent tasks are clear and can be verified in small units.