Skip to content

Latest commit

 

History

History
496 lines (361 loc) · 16 KB

File metadata and controls

496 lines (361 loc) · 16 KB

Developer Notes

Developer: Justin Guida Date: December 11, 2025 Status: Draft

This file explains, in my own words, the main pieces of this project: the Quote / Value JSON model, the QuoteController REST endpoints, and the Spring Boot application setup (entry point, config, tests).

Index

File Role Notes
Quote.java Record for JSON response Shared JSON shape with consumer
Value.java Nested record (id, quote text) Shared JSON shape with consumer
QuoteController.java REST controller, serves quotes Streams, @PathVariable, ThreadLocalRandom
QuoteServiceApplication.java Main Spring Boot application entry point Boots the app on port 8080
application.properties Configuration Default port 8080
QuoteControllerTest.java Tests for all 3 endpoints @WebMvcTest, MockMvc

Quote.java and JSON mapping patterns

Shared JSON shape between quote-service and consuming client.

public record Quote(String type, Value value) { }

This file defines a Quote record. Using a record lets me represent the response with less boilerplate than a normal Java class.

Quote has two fields:

  • type - a String representing the type of response (for example, "success")
  • value - a Value object that holds the id and the actual quote text

Spring, through Jackson, uses this record to map the Java object into JSON when the endpoints return it.

{
  "type": "success",
  "value" : { "id": 1, "quote": "..."}
}

Records replace all that boilerplate - no manual constructors, getters, toString(), equals(), or hashCode(). One line does it all.

There are many ways you can code this, for example using final vs non-final fields, or using getters/setters vs direct field access in simple examples. This is why using records is nice - it reduces the complexity of defining data-holding types, and it simply works with Jackson for JSON mapping.

Example: All-args constructor (manual style)

This is how you can write a class by hand - pass all data up front with new Quote("success", value).

public class Quote {

  private String type;
  private Value value;

  // All-args constructor
  public Quote(String type, Value value) {
    this.type = type;
    this.value = value;
  }

  // Getters
  public String getType() { return type; }
  public Value getValue() { return value; }

  // Optional setters (only if you want mutability)
  public void setType(String type) { this.type = type; }
  public void setValue(Value value) { this.value = value; }
}

Unlike the no-arg + setters pattern that Jackson uses by default (new Quote() then setType(...) / setValue(...)), this style expects all data up front in the constructor call.

Example: No-arg constructor + setters (Jackson default)

This is the pattern Jackson uses by default for JSON deserialization. Jackson calls the no-arg constructor, then uses setters to fill fields.

public class Quote {

  private String type;
  private Value value;

  // No-arg constructor (explicit or omitted; both are fine)
  public Quote() { }

  // Getters
  public String getType() { return type; }
  public Value getValue() { return value; }

  // Setters - Jackson calls these after construction
  public void setType(String type) { this.type = type; }
  public void setValue(Value value) { this.value = value; }
}

Step by step explanation of mutable fields with setters

Step 1: What exists in the class?

Component What it does Code
Fields Data storage inside the object private String type;
private Value value;
Constructor How the object is created public Quote() { }
Setters How we change the fields setType(...), setValue(...)
Getters How we read the fields getType(), getValue()

Summary:

Component Purpose
Fields Data storage inside the object
Constructor How the object is created
Setters How we change the fields
Getters How we read the fields

Step 2: When are the fields "constructed"?

When you call this:

Quote q = new Quote();

This happens:

  1. JVM allocates a new Quote object.
  2. The fields type and value are created inside that object and get default values:
    • type = null
    • value = null
  3. The constructor public Quote() runs (it does nothing extra here).

After new Quote():

  • The object exists.
  • The fields exist, but they are still null.

Key Point: Nothing has called setType or setValue yet.

Step 3: When are the fields "set"?

When you call this:

q.setType("success");
q.setValue(new Value(1L, "some quote"));

This happens:

Method What runs Result
setType("success") this.type = type; type changes from null to "success"
setValue(...) this.value = value; value changes from null to Value instance

Summary:

  • The fields are created at construction (new Quote()).
  • They are filled/changed when setters are called.

Step 4: How does Jackson use this?

When Jackson deserializes JSON into Quote, it does:

  1. Quote q = new Quote(); (calls the no-arg constructor)
  2. q.setType(jsonTypeValue);
  3. q.setValue(jsonValueObject);

Key Point: Same exact steps as above, just done automatically by Jackson.

Step 5: Why can these fields not be final?

What happens to the field type:

When What happens
At construction (new Quote()) type is created with default null
Later, in setType(...) type is assigned a real value

That means the field changes after the object is constructed.

If you wrote:

private final String type;
private final Value value;

Java rules say:

  • A final field must be assigned exactly once, in a constructor or at declaration.
  • You cannot assign to it later in a setter.

So with final:

  • public Quote() { } is illegal unless you set both type and value inside it.
  • setType and setValue would not be allowed to do this.type = ... or this.value = ... because that would change a final field.

This is why:

"This pattern needs non-final fields because we change them after construction."

We change them here:

public void setType(String type) {
  this.type = type;  // this is a change after construction
}

If the field was final, this line would not be allowed.

Final Summary

What When
Object created new Quote() runs
Fields exist At creation, but are null
Fields filled When setters are called
Why not final Setters change fields after construction

Final vs non-final fields for JSON mapping

There are two common ways to map JSON into a Quote class:

  1. Non-final fields with a no-arg constructor and setters (mutable)
  2. Final fields with a constructor only (immutable), which is what records give you

1. Non-final fields + no-arg constructor + setters (mutable)

(This is the same pattern as "Example: No-arg constructor + setters (Jackson default)" above. Repeated here to compare with the final-field version.)

public class Quote {

    private String type;
    private Value value;

    // 1) Jackson calls this no-arg constructor.
    // In Java, if you do not write any constructor, the compiler
    // will create this empty no-arg constructor for you. I am
    // writing it explicitly here to show the "new Quote()" step.
    public Quote() { }

    // 2) Then Jackson calls these setters to fill the fields
    public void setType(String type) {
        this.type = type;
    }

    public void setValue(Value value) {
        this.value = value;
    }

    // Getters so we can read the values
    public String getType() {
        return type;
    }

    public Value getValue() {
        return value;
    }
}

What happens:

  1. Jackson does new Quote() (fields exist but are null).
  2. Jackson calls setType(jsonType) and setValue(jsonValue) to fill the fields.

Key Point: Fields cannot be final here, because setters need to change them after construction.

2. Final fields + constructor only (immutable, no setters)

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public class Quote {

    private final String type;
    private final Value value;

    // Jackson uses THIS constructor directly
    @JsonCreator
    public Quote(
            @JsonProperty("type") String type,
            @JsonProperty("value") Value value) {
        this.type = type;
        this.value = value;
    }

    // Only getters, no setters (object is immutable)
    public String getType() {
        return type;
    }

    public Value getValue() {
        return value;
    }
}

What happens:

  1. Jackson sees @JsonCreator on the constructor.
  2. It reads JSON fields "type" and "value".
  3. It calls new Quote(typeFromJson, valueFromJson).
  4. The fields are set once in the constructor and never change, so they can be final.

Key Point: Here we do not use setters at all. All data comes in through the constructor once.

3. How this relates to record Quote(String type, Value value)

A Java record is basically the second pattern, but the compiler writes the constructor and getters for me.

public record Quote(String type, Value value) { }

The compiler generates something very close to:

public final class Quote {
    private final String type;
    private final Value value;

    public Quote(String type, Value value) {
        this.type = type;
        this.value = value;
    }

    public String type() { return type; }
    public Value value() { return value; }

    // plus equals, hashCode, toString...
}

Summary of the three patterns:

Pattern Description
Non-final + setters Easy, mutable, uses no-arg constructor + setters
Final + constructor Immutable, fields set once in the constructor
Record Final + constructor pattern with less boilerplate

This is why using a record Quote(String type, Value value) is a clean fit for this project. You get the immutable "final field + constructor" style without writing all the boilerplate by hand.


Remaining files (module-specific)

Shared JSON shape between quote-service and consuming client.

Represents the nested "value" part of the JSON: an id and the quote text itself.

public record Value(Long id, String quote) { }

REST controller that serves quotes as JSON. See also concepts/quote-controller.md for detailed explanation.

Key annotations:

  • @RestController - combines @Controller + @ResponseBody, returns JSON directly
  • @RequestMapping("/api") - sets base path for all endpoints
  • @GetMapping - maps HTTP GET to methods
  • @PathVariable - extracts path segments (e.g., /api/5 -> id=5)

Data structure:

private static final List<Value> QUOTES = List.of(...);
  • List.of() creates an immutable list (Java 9+)
  • Static field means quotes are shared across all requests
  • Immutability is thread-safe for concurrent access

Endpoints:

Path Method Returns
/api/ all() All quotes as List
/api/random random() Single random quote
/api/{id} byId() Quote by ID or HTTP 404

Thread-safe random selection:

Value value = QUOTES.get(ThreadLocalRandom.current().nextInt(QUOTES.size()));
  • ThreadLocalRandom avoids contention in concurrent requests
  • Each thread gets its own Random instance
  • See ADR-0003 for rationale

Standard Spring Boot entry point. Nothing special here - just boots the application.

@SpringBootApplication
public class QuoteServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(QuoteServiceApplication.class, args);
    }
}

What @SpringBootApplication does:

  • @Configuration - marks class as bean definition source
  • @EnableAutoConfiguration - enables Spring Boot auto-config
  • @ComponentScan - scans current package for @Component, @RestController, etc.

Configuration for this module.

# Server runs on default port 8080
# No explicit configuration needed for this module
Property Purpose
server.port Default 8080; consuming-rest uses 8081 to avoid conflict

Port configuration:

  • Default: 8080
  • Override with: server.port=8081 in properties
  • Or via command line: ./mvnw spring-boot:run -Dspring-boot.run.arguments=--server.port=8081

Tests the QuoteController using @WebMvcTest to test the web layer in isolation.

@WebMvcTest(QuoteController.class)
class QuoteControllerTest {
    @Autowired MockMvc mockMvc;
}

Key testing patterns:

  • @WebMvcTest - loads only web layer, not full application context (fast)
  • MockMvc - simulates HTTP requests without starting a real server
  • Tests all 3 endpoints: /api/, /api/random, /api/{id}

Tests:

Test What it checks
getAllQuotes_returnsListOf10Quotes /api/ returns 10 quotes with type="success"
getRandomQuote_returnsSuccessWithValidId /api/random returns a valid quote
getQuoteById_withValidId_returnsQuote /api/5 returns the correct quote
getQuoteById_withInvalidId_returns404 /api/999 returns HTTP 404 Not Found