Appearance
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:
| Bump | Means | Example |
|---|---|---|
| MAJOR | Breaking, incompatible API changes | 1.9.9 → 2.0.0 |
| MINOR | New, backward-compatible features | 1.2.0 → 1.3.0 |
| PATCH | Backward-compatible bug fixes | 1.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 lowerTrap — 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)
| Level | Use for |
|---|---|
ERROR | a failure needing attention |
WARN | something suspicious but recoverable |
INFO | high-level lifecycle events (started, processed N) |
DEBUG | diagnostic detail for development |
TRACE | very 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
Under SemVer, going from 1.4.2 to 1.5.0 promises…
Why must version components be compared as numbers, not strings?
A and B pull in the same lib at different versions/depths. How does Maven choose by default?
Why is `log.debug("id {} done", id)` better than `log.debug("id " + id + " done")`?
What does it mean that SLF4J is a 'facade'?
Next: Module 14 · Testing Fundamentals. This module's lab is in labs/src/main/java/com/jse21/m13_build/.