Project Development Using Java and Python

Chapter 5
Professional Java Application Development

Diploma in Programming Languages (Java & Python)
Project Development Using Java and Python   •   Diploma

Professional Java Application Development

Introduction

Professional Java development is more than writing classes that compile. A maintainable application must manage data safely, communicate failure clearly, process information efficiently, coordinate concurrent work, obtain third-party libraries reproducibly, and separate business logic from technical infrastructure. These concerns become increasingly important as a program grows from a small exercise into a service used by many people.

This chapter develops a practical engineering view of modern Java application development. It focuses on the Java Collections Framework, generics, exception design, streams, concurrency, dependency management, and Spring Boot. The examples use Java 17 or later conventions, although most principles also apply to earlier modern Java versions.

Learning Objectives

By the end of this chapter, you should be able to:

  • select suitable collection types for common application requirements;
  • use generics to improve type safety and reduce unnecessary casts;
  • design meaningful exception-handling strategies;
  • distinguish imperative collection processing from stream processing;
  • explain thread safety, executors, futures, and common concurrency hazards;
  • manage project dependencies and builds with Maven;
  • describe inversion of control and dependency injection;
  • construct a small Spring Boot service using layered design;
  • evaluate maintainability, security, testing, and operational concerns.

1. Maintainability as a Design Goal

Maintainability is the ease with which software can be understood, corrected, tested, extended, and operated. It is influenced by several related qualities:

QualityMeaning in practice
ReadabilityAnother developer can understand the code and its assumptions.
ModularityA change in one component has limited impact on others.
TestabilityBehavior can be verified without excessive infrastructure.
ReliabilityFailures are detected and handled predictably.
ObservabilityLogs, metrics, and traces reveal system behavior.
SecurityInputs, dependencies, identities, and data are protected.

A useful professional principle is to make invalid states difficult to represent. For example, a method that accepts a List<String> may receive null elements, duplicate identifiers, or incorrectly formatted values. A stronger design validates input at a boundary and then uses domain objects whose invariants are explicit.

A maintainable Java component generally has one clear responsibility, a small public interface, controlled mutable state, meaningful names, and tests that focus on observable behavior. Comments should explain decisions and constraints rather than restating syntax.

2. Collections and Data Structures

The Collections Framework provides standard interfaces and implementations for groups of objects. The principal interfaces are List, Set, Queue, Deque, and Map. Choosing a collection is a design decision, not merely a coding preference.

2.1 Core collection types

A List<E> is ordered and permits duplicates. ArrayList<E> is normally the default list because indexed access is efficient and appending is usually efficient. LinkedList<E> can support efficient insertion at an end, but its node allocation and poor cache locality mean it is rarely the best general-purpose choice.

A Set<E> represents unique elements. HashSet<E> provides fast average-case membership operations when correctly implemented equals and hashCode methods are used. LinkedHashSet<E> preserves insertion order. TreeSet<E> maintains sorted order and requires elements to be comparable or supplied with a comparator.

A Map<K,V> associates keys with values. HashMap<K,V> is the usual choice for key lookup. LinkedHashMap<K,V> preserves insertion order and can also support access-order arrangements useful for cache policies. TreeMap<K,V> maintains sorted keys. ConcurrentHashMap<K,V> supports concurrent access with operations designed for multithreaded programs.

Queue<E> models waiting work, while Deque<E> supports insertion and removal at both ends. ArrayDeque<E> is usually preferable to the legacy Stack class for stack behavior.

2.2 Complexity and practical choice

Big-O notation describes how resource requirements grow as input size increases. For example, average lookup in a hash table is commonly described as O(1), whereas searching an unsorted list is O(n), where n is the number of elements. These are asymptotic descriptions, not guarantees of identical real-world speed. Hash quality, memory locality, allocation, and workload distribution also matter.

RequirementSuitable choiceImportant qualification
Ordered, indexed valuesArrayListInsertion near the beginning may shift many elements.
Unique values, no ordering requirementHashSetCorrect equality and hashing are essential.
Unique values in insertion orderLinkedHashSetUses more memory than HashSet.
Sorted valuesTreeSetOperations are typically O(log n).
Key-value lookupHashMapNot thread-safe and does not guarantee order.
Shared concurrent key-value stateConcurrentHashMapCompound actions still require careful design.
FIFO or double-ended processingArrayDequeDo not use it concurrently without external coordination.

2.3 Equality, hashing, and mutability

Hash-based collections rely on a contract: if two objects are equal according to equals, they must return the same hashCode. A key should not be mutated in a way that changes equality or hashing while it is stored in a hash-based collection. Otherwise, the entry may remain present but become difficult or impossible to find.

Modern Java records are useful for small immutable data carriers:

public record CustomerId(String value) {
    public CustomerId {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException("Customer ID is required");
        }
    }
}

Records automatically provide value-based equals, hashCode, and toString implementations. They do not make referenced objects deeply immutable, so the designer must still consider the mutability of each component.

3. Generics and Type Safety

Generics allow a type to be parameterized. List<String> communicates that the list is intended to contain strings and enables the compiler to detect many errors before execution. Without generics, values are often treated as Object, creating casts and increasing runtime risk.

List<String> names = new ArrayList<>();
names.add("Amina");
String first = names.get(0);

The diamond operator <> allows the compiler to infer type arguments in many declarations. Generic types in Java are invariant: a List<Integer> is not a subtype of List<Number>, even though Integer is a subtype of Number.

3.1 Bounded wildcards

The principle “producer extends, consumer super” is a useful guide. A method that reads values can accept ? extends T; a method that writes values can accept ? super T.

static double total(List<? extends Number> values) {
    double result = 0.0;
    for (Number value : values) {
        result += value.doubleValue();
    }
    return result;
}

static void addDefaults(List<? super Integer> values) {
    values.add(0);
}

? extends Number means that the actual list has some specific subtype of Number; values can safely be read as Number, but arbitrary numbers cannot safely be added. ? super Integer permits adding integers because the actual list is guaranteed to accept them, although retrieved values can only safely be treated as Object.

Generic methods can express relationships among parameters:

static <T> T firstOrThrow(List<T> items) {
    if (items.isEmpty()) {
        throw new IllegalArgumentException("At least one item is required");
    }
    return items.get(0);
}

Java implements generics mainly through type erasure. Consequently, code cannot normally use new T(), cannot test value instanceof T, and cannot create arrays of a type variable directly. These limitations should be understood when designing generic libraries.

4. Exceptions and Failure Design

An exception represents an abnormal condition that interrupts normal control flow. Checked exceptions extend Exception but not RuntimeException; callers must catch or declare them. Unchecked exceptions extend RuntimeException; they commonly indicate programming errors, invalid arguments, or failures that callers cannot reasonably recover from at every call site.

The choice should express a recovery contract. A file-import component might expose a checked or domain-specific failure if the caller can retry, report, or select another file. A method receiving a null argument may throw IllegalArgumentException because the caller must correct its programming or validation error.

public Order loadOrder(OrderId id) throws OrderNotFoundException {
    return repository.findById(id)
        .orElseThrow(() -> new OrderNotFoundException(id));
}

4.1 Good exception practices

  1. Catch the narrowest exception that can be handled.
  2. Do not catch an exception merely to suppress it.
  3. Preserve the original cause when translating exceptions.
  4. Avoid exposing passwords, tokens, personal data, or internal stack traces to users.
  5. Include useful context such as an order identifier, but avoid sensitive information.
  6. Use try-with-resources for AutoCloseable resources.
try (BufferedReader reader = Files.newBufferedReader(path)) {
    return reader.lines().toList();
} catch (IOException ex) {
    throw new ImportException("Could not read customer file", ex);
}

A finally block is appropriate for cleanup that must occur regardless of success, but try-with-resources is safer for closable resources because it handles suppressed exceptions correctly.

Exception handling should occur at an architectural boundary. A repository may translate a database exception into a persistence exception; a web controller or exception handler may then translate a domain failure into an appropriate HTTP response. Business logic should not be filled with HTTP-specific details.

5. Streams and Functional-Style Processing

A stream is a sequence-oriented pipeline for processing data. It is not a data structure and does not store elements independently. A stream pipeline usually has a source, zero or more intermediate operations, and one terminal operation.

List<String> activeNames = customers.stream()
    .filter(Customer::active)
    .map(Customer::name)
    .sorted()
    .toList();

filter, map, flatMap, distinct, sorted, limit, and skip are common intermediate operations. toList, collect, reduce, count, findFirst, anyMatch, and forEach are terminal operations. Intermediate operations are lazy: they do not process elements until a terminal operation is invoked.

Streams can improve clarity when a transformation is naturally expressed as a pipeline. They are not automatically faster or more readable than a loop. Avoid streams when logic involves complex state, multiple side effects, difficult debugging, or control flow that becomes obscure.

5.1 Reduction and collectors

A reduction combines elements into one result. For example, summing values can be expressed as:

int total = quantities.stream()
    .mapToInt(Integer::intValue)
    .sum();

A collector can group objects:

Map<Status, List<Order>> byStatus = orders.stream()
    .collect(Collectors.groupingBy(Order::status));

Parallel streams divide work across the common fork-join pool. They are appropriate only after measurement and only when operations are sufficiently large, independent, associative, and free from unsafe shared mutation. Database calls, network calls, and small collections are commonly poor candidates.

6. Concurrency and Thread Safety

Concurrency means that multiple tasks can make progress during overlapping periods. Parallelism means that tasks execute simultaneously, typically on multiple processor cores. A program can be concurrent without being parallel.

Shared mutable state creates the central difficulty. A race condition occurs when the result depends on an uncontrolled timing relationship. An operation such as count++ is not one indivisible action; it involves reading, adding, and writing. A volatile variable provides visibility guarantees for a variable, but it does not generally make compound operations atomic.

6.1 Executors, futures, and tasks

Application code should normally submit tasks to an executor rather than create an unbounded number of raw threads.

try (ExecutorService executor = Executors.newFixedThreadPool(4)) {
    Future<String> result = executor.submit(() -> fetchCustomerName(42L));
    System.out.println(result.get());
}

Future.get() may block and may throw InterruptedException or ExecutionException. When catching InterruptedException, a task that cannot complete should usually restore the interrupt status:

catch (InterruptedException ex) {
    Thread.currentThread().interrupt();
    throw new CancellationException("Task interrupted");
}

CompletableFuture supports composition of asynchronous operations, but error paths must be designed explicitly. Modern Java also provides virtual threads, which can be useful for large numbers of mostly blocking tasks. They do not make CPU-intensive work faster, eliminate synchronization requirements, or remove the need for bounded external resources such as database connections.

6.2 Synchronization strategies

Possible strategies include:

  • immutable objects, which avoid shared mutation;
  • confinement, where state belongs to one thread or request;
  • synchronized blocks or methods;
  • Lock implementations when advanced coordination is required;
  • atomic classes such as AtomicInteger for suitable single-variable operations;
  • concurrent collections such as BlockingQueue and ConcurrentHashMap.

Deadlock can occur when threads wait permanently for locks held by one another. Consistent lock ordering, short critical sections, and avoiding calls to unknown code while holding a lock reduce the risk. Thread safety must be documented at the component boundary rather than assumed from individual methods.

7. Dependency Management with Maven

A Java project depends on libraries, plugins, a compiler version, test tools, and packaging rules. Maven describes these in pom.xml and resolves transitive dependencies from repositories.

<properties>
    <maven.compiler.release>17</maven.compiler.release>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

Dependency scopes communicate intended use. A compile dependency is needed to build and run the application; a test dependency is needed only for tests; runtime dependencies may not be required to compile application code. Spring Boot commonly uses a parent or Bill of Materials to manage compatible versions.

Professional dependency management requires more than adding packages. Teams should:

  • commit the build descriptor and use reproducible version control;
  • avoid unnecessary libraries and transitive complexity;
  • update dependencies regularly;
  • scan for known vulnerabilities;
  • verify licenses and organizational policies;
  • use dependency locking or reproducible build practices where appropriate;
  • separate application code from generated build output.

A dependency vulnerability is a governance issue as well as a technical issue. Security scanning does not replace code review, threat modelling, or timely patching.

8. Spring Boot and Application Architecture

Spring Boot simplifies creation of stand-alone Java applications by providing auto-configuration, embedded servers, configuration conventions, and production-oriented facilities. Its central design idea is inversion of control: instead of application classes constructing all collaborators themselves, a container creates and supplies them.

Dependency injection makes dependencies visible:

@Service
public class OrderService {
    private final OrderRepository repository;

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

    public Order find(OrderId id) {
        return repository.findById(id)
            .orElseThrow(() -> new OrderNotFoundException(id));
    }
}

Constructor injection is generally preferred because required dependencies are explicit, fields can be final, and the class is easier to test. Interfaces are useful where substitution represents a real design need; creating interfaces for every class without a purpose can add unnecessary complexity.

A conventional layered service may contain:

  • Controller: translates HTTP requests and responses;
  • Application or service layer: coordinates use cases and business rules;
  • Domain model: represents business concepts and invariants;
  • Repository: abstracts persistence operations;
  • Infrastructure: implements databases, messaging, external clients, and configuration.

A controller should not contain substantial business rules or database queries. Data-transfer objects can prevent external representations from becoming tightly coupled to persistence entities.

@RestController
@RequestMapping("/orders")
class OrderController {
    private final OrderService service;

    OrderController(OrderService service) {
        this.service = service;
    }

    @GetMapping("/{id}")
    OrderResponse get(@PathVariable long id) {
        return OrderResponse.from(service.find(new OrderId(id)));
    }
}

Validation should occur at appropriate boundaries. Authentication verifies identity; authorization determines whether an identified user may perform an action. These must not be confused. Secrets should be supplied through secure configuration mechanisms rather than committed to source control.

9. Applied Case: Illustrative Order-Processing Service

Background

An online retailer needs a Java service that retrieves orders, validates new orders, and reports failures consistently. The initial prototype uses maps in controllers, catches generic exceptions, and creates a new database connection for each request.

Problem

The prototype is difficult to test, duplicate order identifiers are possible, concurrent requests sometimes overwrite state, and dependency versions differ between developer machines.

Analysis

The design problems are structural. Collections are being used without clear invariants; persistence, HTTP, and business decisions are mixed; exceptions are not translated at boundaries; and resource ownership is unmanaged. The system also lacks a defined concurrency policy.

Approach and solution

The team introduces an immutable OrderId value object and an Order domain object. An OrderRepository interface hides persistence details. OrderService validates the order and applies business rules. OrderController handles HTTP mapping only. Constructor injection makes each dependency explicit.

A HashMap is retained only for a single-threaded test repository. The production repository uses the database transaction mechanism. Where an in-memory cache is required, ConcurrentHashMap is used with an atomic operation such as computeIfAbsent, while cache expiration and memory limits are handled separately.

Maven manages Spring Boot, database, and test dependencies. The build runs unit tests and dependency checks in continuous integration. A centralized exception handler maps a missing order to a client-facing not-found response and unexpected failures to a generic server error, while logging a correlation identifier rather than sensitive data.

Outcome

The service becomes easier to test because business logic can run without an HTTP server or database. The design also makes replacement of the repository possible, limits controller complexity, and provides a consistent failure contract. Performance is measured with representative workloads rather than assumed from the use of streams or concurrency.

Learning points

  1. A collection choice must follow an explicit access and ownership requirement.
  2. Immutability reduces accidental sharing and synchronization demands.
  3. Dependency injection supports substitution and testing, but it does not replace sound architecture.
  4. Exception translation should occur at boundaries.
  5. Concurrency must be designed around state ownership, atomicity, and resource limits.
  6. Build reproducibility and dependency security are part of professional development.

10. Testing, Observability, and Operational Practice

Unit tests should verify domain and service behavior quickly. Integration tests should verify collaboration with real or realistically configured infrastructure. End-to-end tests should be limited to important user journeys because they are slower and more fragile.

A professional service should expose useful logs, metrics, and health information. Logs should be structured where possible and should not contain credentials, payment data, or unnecessary personal information. Metrics can reveal request rates, error rates, latency, queue size, and resource saturation. Tracing helps connect work across services.

Testing concurrent code requires more than one successful run. Tests should examine invariants, cancellation, timeouts, failure propagation, and shutdown behavior. Avoid arbitrary sleeps as synchronization mechanisms; use latches, barriers, futures, or other explicit coordination tools.

Challenges, Limitations, and Contemporary Developments

Collections hide implementation details but do not remove memory constraints. Streams can obscure control flow and may perform poorly when misused. Generic type safety is limited by type erasure. Exceptions can become confusing if a project lacks a consistent error taxonomy. Concurrency can improve throughput while making failures nondeterministic and debugging difficult.

Spring Boot accelerates development but introduces framework conventions, reflection, configuration complexity, and a substantial dependency surface. Auto-configuration should be understood rather than accepted blindly. Framework-managed components still require clear boundaries and tests.

Current Java development increasingly includes records, sealed classes, pattern matching, virtual threads, containerized deployment, automated software composition analysis, and AI-assisted coding tools. AI-generated code must be reviewed for correctness, security, licensing, privacy, and architectural fit. Developers remain accountable for the behavior of code they submit.

Best-Practice Checklist

  • Choose collections according to ordering, uniqueness, access, ownership, and concurrency requirements.
  • Define equality and hashing consistently; prefer immutable keys.
  • Use generics and bounded wildcards to express safe APIs.
  • Validate input at boundaries and preserve exception causes.
  • Prefer clear loops over clever streams when the logic is stateful or complex.
  • Use managed executors and explicit cancellation and shutdown policies.
  • Avoid unsynchronized shared mutable state.
  • Pin and regularly review dependency versions.
  • Prefer constructor injection and separate web, application, domain, and infrastructure concerns.
  • Test behavior at unit, integration, and system levels.
  • Protect secrets and personal data in logs, configuration, and error responses.
  • Measure performance before introducing parallelism or caching.

Chapter Summary

Professional Java application development combines language features with disciplined engineering. Collections model groups of data, and their correct use depends on ordering, uniqueness, lookup, mutability, and concurrency requirements. Generics provide compile-time type safety, while bounded wildcards help design flexible producer and consumer APIs. Exceptions should represent meaningful failure contracts, preserve causes, and be translated at architectural boundaries.

Streams offer lazy, composable data processing, but they are not a universal replacement for loops and should be used cautiously with side effects and parallel execution. Concurrency requires explicit attention to visibility, atomicity, ownership, cancellation, deadlock, and resource limits. Maven provides reproducible dependency and build management, while Spring Boot supports inversion of control, dependency injection, layered architecture, configuration, and operational deployment.

The central professional lesson is that maintainability results from coherent boundaries: data structures match requirements, components expose small contracts, failures are deliberate, dependencies are governed, and operational behavior is observable and testable.

Key Terms

Collection: An object used to group and manage multiple values.

List: An ordered collection that may contain duplicates.

Set: A collection whose elements are unique according to equality rules.

Map: A structure that associates keys with values.

Generics: Java’s mechanism for parameterized, compile-time type-safe classes and methods.

Wildcard: A flexible generic type argument such as ? extends T or ? super T.

Type erasure: The transformation by which most generic type information is removed at runtime.

Exception: An object representing an abnormal condition or failure.

Try-with-resources: Syntax that automatically closes AutoCloseable resources.

Stream: A lazy pipeline for processing a sequence of values.

Collector: An object or operation that accumulates stream results.

Concurrency: Overlapping progress of multiple tasks.

Race condition: Incorrect behavior caused by uncontrolled timing between concurrent operations.

Executor: A component that manages task execution and worker resources.

Immutability: A property whereby an object’s state cannot change after construction.

Dependency management: The controlled declaration, resolution, updating, and auditing of libraries.

Inversion of control: Delegation of object creation and coordination to a framework or container.

Dependency injection: Supplying a component’s collaborators from outside the component.

Repository: An abstraction for storing and retrieving domain objects.

Observability: The ability to understand system behavior through logs, metrics, and traces.

Review Questions

Short Answer Questions

  1. What distinguishes a List from a Set?
  2. Why should mutable objects generally not be used as keys in a HashMap?
  3. Explain the producer-extends, consumer-super principle.
  4. What is the purpose of try-with-resources?
  5. Why is constructor injection commonly preferred in Spring applications?

Long Answer Questions

  1. Compare ArrayList, LinkedList, HashSet, TreeSet, HashMap, and ConcurrentHashMap, including suitable use cases and limitations.
  2. Explain Java stream pipelines, including intermediate and terminal operations, laziness, collectors, and limitations of parallel streams.
  3. Discuss exception design in a layered application, from repository failure to client-facing response.
  4. Explain the principal causes of concurrency errors and evaluate strategies for reducing shared mutable state.
  5. Describe how Maven and Spring Boot contribute to reproducible, maintainable application development.

Analytical / Application Questions

  1. A service must preserve insertion order, reject duplicate product codes, and support occasional fast membership checks. Which collection design would you choose, and what trade-offs would you explain?
  2. An endpoint performs three independent remote calls and combines their results. Design a concurrency approach using managed Java executors or CompletableFuture, including timeout, cancellation, and failure handling.
  3. Review a controller that validates business rules, directly accesses a database, catches Exception, and returns stack traces to clients. Identify the maintainability and security problems and propose a layered redesign.

Multiple Choice Questions with Answers

Question 1

Which collection is most appropriate when unique elements must also retain insertion order?

A. HashSet

B. LinkedHashSet

C. TreeSet

D. ArrayList

Correct Answer: B. LinkedHashSet

Question 2

What does List<? extends Number> safely allow a method to do?

A. Add arbitrary Number objects

B. Read elements as Number values

C. Cast the list to List<Integer>

D. Create new elements of the unknown subtype

Correct Answer: B. Read elements as Number values

Question 3

Which statement about stream intermediate operations is correct?

A. They execute immediately when declared

B. They always create a new collection

C. They are generally lazy until a terminal operation runs

D. They can only be used with parallel streams

Correct Answer: C. They are generally lazy until a terminal operation runs

Question 4

What is the main purpose of try-with-resources?

A. To retry failed operations automatically

B. To close AutoCloseable resources automatically

C. To convert checked exceptions into errors

D. To make all code thread-safe

Correct Answer: B. To close AutoCloseable resources automatically

Question 5

Why is count++ unsafe when multiple threads update the same ordinary variable?

A. It is a compound read-modify-write operation

B. It always causes a deadlock

C. It cannot be compiled in a concurrent program

D. It changes the variable’s data type

Correct Answer: A. It is a compound read-modify-write operation

Question 6

Which Maven scope is normally used for a library required only by tests?

A. compile

B. provided

C. runtime

D. test

Correct Answer: D. test

Question 7

What is a principal benefit of constructor injection?

A. It hides required dependencies

B. It makes required dependencies explicit

C. It prevents unit testing

D. It forces all classes to be static

Correct Answer: B. It makes required dependencies explicit

Question 8

Which component should normally coordinate business use cases in a layered Spring application?

A. Service or application layer

B. HTTP server socket

C. Maven compiler

D. Logging formatter

Correct Answer: A. Service or application layer

Question 9

What must be true of equals and hashCode for objects used in hash-based collections?

A. Equal objects must have equal hash codes

B. Every object must have a unique hash code

C. Hash codes must never be overridden

D. Equals must compare object identity only

Correct Answer: A. Equal objects must have equal hash codes

Question 10

Which practice best reduces the risk of exposing sensitive information through application errors?

A. Return full stack traces to every client

B. Log passwords for troubleshooting

C. Translate failures into safe responses and keep detailed causes in protected logs

D. Suppress every exception silently

Correct Answer: C. Translate failures into safe responses and keep detailed causes in protected logs

Project Development Using Java and Python
Chapter 5 of 15