Skip to content

Lesson 03 · Dependencies & Logging

Beyond the 1Z0-830 exam

Two everyday-developer skills the exam ignores: understanding how versions and transitive dependencies resolve (and conflict), and logging properly instead of System.out.println.

Objectives

After this lesson you will be able to:

  • Read semantic versions and know what each bump promises.
  • Reason about transitive dependencies and how a build resolves conflicts.
  • Use SLF4J with appropriate log levels and parameterized messages.

Semantic versioning

MAJOR.MINOR.PATCH (e.g. 5.11.3) encodes a promise about compatibility:

BumpMeansExample
MAJORBreaking, incompatible API changes1.9.9 → 2.0.0
MINORNew, backward-compatible features1.2.0 → 1.3.0
PATCHBackward-compatible bug fixes1.2.3 → 1.2.4

So upgrading within the same MAJOR should be safe; a MAJOR bump warns you to read the changelog.

Gotcha

Versions are numeric per component, not lexical strings: 1.2.10 > 1.2.9. A naive string sort would put "1.2.10" before "1.2.9". The lab's SemVer.compareTo compares each component as an int for exactly this reason.

Transitive dependencies and conflicts

Your dependencies have dependencies. If A needs lib v1.0 and B needs lib v2.0, only one version can be on the classpath — a conflict.

How Maven resolves it: "nearest definition wins" — the version at the shallowest depth in the dependency tree, not the highest version.

your-project
├── A → lib 1.0        (depth 1)  ← nearest, so 1.0 wins
└── B → C → lib 2.0    (depth 3)

The lab models this:

java
SemVer chosen = SemVer.nearestWins(List.of(
    new Resolved(parse("2.0.0"), 3),
    new Resolved(parse("1.0.0"), 1)));   // → 1.0.0 (depth 1), even though it's lower

Trap — nearest, not highest

It surprises people that Maven can pick the lower version. (Gradle differs — it defaults to highest of the conflicting versions.) Either way, when a conflict bites, pin the version explicitly — Maven's <dependencyManagement> or a direct dependency declaration — rather than hoping resolution picks the right one. Inspect the tree with mvn dependency:tree.

Logging — not println

System.out.println has no levels, no timestamps, no routing, and no way to turn it off in production. Use the SLF4J facade with a backend like Logback:

java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class OrderService {
    private static final Logger log = LoggerFactory.getLogger(OrderService.class);

    void place(Order o) {
        log.debug("placing order {}", o.id());   // {} is filled only if DEBUG is enabled
        try {
            // ...
        } catch (Exception e) {
            log.error("order {} failed", o.id(), e); // last arg = the throwable → full stack trace
        }
    }
}

SLF4J is a facade: your code compiles against the SLF4J API, and you drop in a backend (Logback, Log4j2) at runtime — swap implementations without touching code.

Log levels (high to low severity)

LevelUse for
ERRORa failure needing attention
WARNsomething suspicious but recoverable
INFOhigh-level lifecycle events (started, processed N)
DEBUGdiagnostic detail for development
TRACEvery fine-grained, noisy detail

You set a threshold (say INFO) and everything at that level or higher is emitted; DEBUG/ TRACE are suppressed. That's the whole point — verbosity is a config change, not a code change.

Gotcha — parameterized messages

Use log.debug("user {} did {}", id, action), not log.debug("user " + id + " did " + action). With {} placeholders, SLF4J skips building the string entirely when DEBUG is disabled; string concatenation pays the cost every call regardless of level.

SDET note

In tests, asserting on log output is brittle — prefer asserting on behavior/return values. When you must verify logging (e.g. an audit requirement), capture it with a test appender or a library like slf4j-test, and keep test log levels low so a failing CI run has diagnostics.

Key Takeaways

  • SemVer: MAJOR = breaking, MINOR = compatible features, PATCH = fixes. Compare numerically per component (1.2.10 > 1.2.9).
  • Transitive conflicts are inevitable; Maven picks "nearest" (shallowest), Gradle picks highest — when it matters, pin explicitly and inspect mvn dependency:tree.
  • Log via the SLF4J facade + a backend (Logback); pick a backend at runtime without code changes.
  • Use levels (ERROR→TRACE) and a configurable threshold; verbosity is config, not code.
  • Always use {} parameterized messages and pass a throwable as the last arg for the stack trace.

Lesson Quiz

Lesson Quiz · Dependencies & Logging0 / 5
  1. Under SemVer, going from 1.4.2 to 1.5.0 promises…

    • ABreaking changes
    • BNew backward-compatible features
    • COnly bug fixes
    • DNothing
  2. Why must version components be compared as numbers, not strings?

    • AThey are always single digits
    • BLexically '1.2.10' sorts before '1.2.9', which is wrong — 1.2.10 is newer
    • CStrings can't be compared
    • DIt's faster
  3. A and B pull in the same lib at different versions/depths. How does Maven choose by default?

    • AHighest version always
    • BNearest definition (shallowest depth) wins
    • CLowest version always
    • DIt fails the build
  4. Why is `log.debug("id {} done", id)` better than `log.debug("id " + id + " done")`?

    • AIt's shorter only
    • BWith {} the message string is built only when DEBUG is enabled; concatenation always pays the cost
    • CConcatenation doesn't compile
    • DThey are identical
  5. What does it mean that SLF4J is a 'facade'?

    • AIt logs to the console only
    • BYou code against its API and choose a backend (Logback/Log4j2) at runtime without changing code
    • CIt is part of the JDK
    • DIt replaces the need for log levels

Next: Module 14 · Testing Fundamentals. This module's lab is in labs/src/main/java/com/jse21/m13_build/.