Series MapLesson 22 / 112
Focus mode active/Press Alt+Shift+R to toggle/Esc to exit
Build CoreOrdered learning track

Content Negotiation and HTTP Caching

Content Negotiation CORS Compression Cache Control and ETag

HTTP negotiation dan caching behavior untuk JAX-RS production APIs: media type, CORS, compression, Cache-Control, ETag, conditional request, proxy/gateway interaction, dan PR review

7 min read1321 words
PrevNext
Lesson 22112 lesson track22–61 Build Core
#http#cors#compression#cache-control+4 more

Part 022 — Content Negotiation, CORS, Compression, Cache-Control, and ETag

Fokus part ini: memahami metadata HTTP yang mengontrol representation, browser access, compression, caching, dan conditional request pada API production.

Part sebelumnya membahas status code dan header strategy.

Sekarang kita fokus pada header yang sering tampak “sekunder”, tetapi di production bisa menjadi sumber bug besar:

- Content-Type
- Accept
- Accept-Encoding
- Content-Encoding
- Origin
- Access-Control-*
- Cache-Control
- ETag
- If-Match
- If-None-Match
- Vary

Dalam API JAX-RS, sebagian behavior ini terlihat dari annotation seperti:

@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)

Tetapi behavior production sebenarnya bisa dipengaruhi banyak layer:

client
API gateway / APIM
load balancer
ingress
service mesh
Servlet container
JAX-RS runtime
filters/interceptors
MessageBodyReader/Writer
CDN/proxy/cache

Karena itu, senior engineer tidak cukup tahu annotation. Senior engineer harus tahu jalur lifecycle-nya.


1. Representation Is Not the Resource

Resource adalah konsep.

Representation adalah bentuk data yang dikirim melalui HTTP.

Example:

Resource: Quote Q-1001
Representation 1: application/json
Representation 2: application/xml
Representation 3: application/pdf
Representation 4: text/csv

JAX-RS resource method mengembalikan Java object, tetapi HTTP response mengirim representation.

@GET
@Path("/{quoteId}")
@Produces(MediaType.APPLICATION_JSON)
public QuoteResponse getQuote(@PathParam("quoteId") String quoteId) {
    return quoteService.getQuote(quoteId);
}

Pipeline:

QuoteResponse Java object
-> MessageBodyWriter
-> JSON bytes
-> HTTP response body
-> Content-Type: application/json

Bug sering terjadi karena engineer menganggap Java object adalah contract. Yang menjadi contract bagi client adalah representation.


2. Content-Type vs Accept

Content-Type menjelaskan body yang dikirim.

Content-Type: application/json

Accept menjelaskan response yang diminta client.

Accept: application/json

For request with body:

POST /quotes HTTP/1.1
Content-Type: application/json
Accept: application/json

Meaning:

I am sending JSON.
I want JSON back.

Common confusion:

Content-Type is not the same as Accept.

If request body is JSON but Content-Type missing, JAX-RS may not select the expected MessageBodyReader.

If Accept asks for XML but endpoint only produces JSON, runtime may return 406 Not Acceptable.

If Content-Type is XML but endpoint only consumes JSON, runtime may return 415 Unsupported Media Type.


3. JAX-RS Content Negotiation Lifecycle

Simplified lifecycle:

flowchart TD A[HTTP Request] --> B[Match Path] B --> C[Match HTTP Method] C --> D[Match Consumes using Content-Type] D --> E[Match Produces using Accept] E --> F[Select Resource Method] F --> G[Read Entity using MessageBodyReader] G --> H[Invoke Resource Method] H --> I[Write Entity using MessageBodyWriter] I --> J[HTTP Response]

Important:

Path/method may match, but media type matching can still fail.

Typical failures:

404 -> path mismatch
405 -> method mismatch
415 -> request Content-Type unsupported
406 -> response Accept unsupported
500 -> provider selected but serialization fails

4. @Consumes Strategy

@Consumes declares what request body media types resource can consume.

Example:

@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response createQuote(CreateQuoteRequest request) {
    ...
}

Good practice:

Declare @Consumes for methods with request body.
Avoid accepting everything unless there is a reason.

Bad:

@POST
public Response createQuote(String rawBody) {
    ...
}

Why risky:

- weak contract
- validation harder
- provider selection less clear
- generated contract weaker
- client integration more error-prone

When multiple content types are supported:

@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })

Senior review question:

Does this endpoint truly support multiple media types, or is this accidental flexibility?

5. @Produces Strategy

@Produces declares what response media types endpoint can produce.

Example:

@GET
@Path("/{quoteId}")
@Produces(MediaType.APPLICATION_JSON)
public QuoteResponse getQuote(@PathParam("quoteId") String quoteId) {
    return quoteService.getQuote(quoteId);
}

If supporting export:

@GET
@Path("/{quoteId}/documents/proposal")
@Produces("application/pdf")
public Response downloadProposal(@PathParam("quoteId") String quoteId) {
    StreamingOutput pdf = quoteDocumentService.streamProposal(quoteId);
    return Response.ok(pdf)
        .type("application/pdf")
        .header("Content-Disposition", "attachment; filename=quote-proposal.pdf")
        .build();
}

Do not rely only on body shape.

Client needs Content-Type to decode correctly.


6. Vendor Media Types and Versioning

Some enterprise APIs use vendor media types:

Accept: application/vnd.company.quote.v1+json

Potential benefit:

- versioning via media type
- explicit representation contract

Potential cost:

- harder client setup
- harder testing
- more provider configuration
- less friendly to generic tooling

Most teams choose one of:

URI versioning: /v1/quotes
Header versioning: API-Version: 1
Media type versioning: application/vnd.x.v1+json

Internal verification:

- Which versioning strategy is official?
- Is media type versioning used anywhere?
- Does OpenAPI represent it accurately?

7. Default Media Type Risk

Some frameworks infer default media type.

This can hide bugs.

Example:

Client forgets Accept header.
Server returns JSON due to default.
Everything seems fine.
Later another client sends Accept: */* and receives unexpected representation.

Senior guidance:

Be explicit in public/enterprise endpoints.
Use defaults intentionally, not accidentally.

Internal verification:

- Does internal platform enforce JSON-only APIs?
- Are @Produces/@Consumes mandatory in code review?
- Does API linting require content types?

8. CORS Mental Model

CORS is a browser security mechanism.

It is not a general service-to-service security model.

CORS answers:

May browser JavaScript from Origin A call resource at Origin B?

Example browser request:

Origin: https://portal.example.com

Server response:

Access-Control-Allow-Origin: https://portal.example.com

If browser does not accept CORS policy, it blocks response from JavaScript.

Important:

CORS does not stop non-browser clients like curl, backend services, or Postman.

Authentication and authorization are still required.


9. Simple Request vs Preflight Request

Some cross-origin requests trigger preflight.

Browser sends:

OPTIONS /quotes HTTP/1.1
Origin: https://portal.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization,content-type

Server should respond:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://portal.example.com
Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS
Access-Control-Allow-Headers: authorization,content-type,x-correlation-id
Access-Control-Max-Age: 600

Failure mode:

POST endpoint is correct, but browser never sends POST because preflight fails.

Debugging clue:

Backend logs may show only OPTIONS, not POST.

10. CORS Placement: Service vs Gateway

CORS can be handled at:

- application filter
- Servlet filter
- API gateway/APIM
- ingress
- reverse proxy

Do not implement duplicate CORS logic in multiple layers without clear ownership.

Failure mode:

Service allows origin, gateway strips header.
Gateway allows origin, service rejects OPTIONS.
Two layers emit conflicting Access-Control-Allow-Origin.

Internal verification checklist:

- Is CORS service-owned or gateway-owned?
- Are allowed origins environment-specific?
- Are credentials allowed?
- Are wildcard origins prohibited for authenticated APIs?
- Are OPTIONS requests authenticated or bypassed?

11. CORS and Credentials

Credentialed browser requests require careful policy.

Example:

Access-Control-Allow-Credentials: true

If credentials are allowed, wildcard origin is unsafe and generally invalid for browser enforcement.

Bad:

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

Better:

Access-Control-Allow-Origin: https://trusted-portal.example.com
Access-Control-Allow-Credentials: true

Senior review:

If API is authenticated and browser-accessible, CORS must be strict and environment-aware.

12. Compression Mental Model

Compression reduces payload size but adds CPU cost and can affect streaming.

Client request:

Accept-Encoding: gzip, br

Server response:

Content-Encoding: gzip
Vary: Accept-Encoding

Compression can be handled by:

- application
- Servlet container
- ingress/gateway
- CDN/proxy

Do not assume JAX-RS itself is the compression layer.


13. Compression Trade-Offs

Benefits:

- lower bandwidth
- faster large JSON transfer
- lower egress cost

Costs:

- CPU overhead
- latency for small payloads
- complexity with streaming
- harder raw packet debugging
- possible security concern for secret-reflecting responses

Common rule:

Compress large text responses.
Do not compress already-compressed binaries such as PDF/image/zip.
Be cautious with tiny payloads.

Internal verification:

- Which layer performs compression?
- What minimum response size triggers compression?
- Are streaming responses compressed?
- Are binary responses excluded?

14. Compression and Streaming

Streaming response plus compression can behave unexpectedly.

Possible issue:

Application flushes chunks.
Compression layer buffers chunks.
Client does not receive data until buffer fills.

This can break:

- SSE
- long-running progress stream
- chunked export
- heartbeat response

Senior review:

For streaming endpoint, verify compression behavior explicitly.

For SSE, often use:

Cache-Control: no-cache
Content-Type: text/event-stream

and ensure proxy buffering/compression does not break event delivery.


15. Cache-Control Mental Model

Caching is not just performance. Caching is correctness.

Cache-Control tells clients/proxies how to store and reuse response.

Examples:

Cache-Control: no-store
Cache-Control: no-cache
Cache-Control: private, max-age=60
Cache-Control: public, max-age=3600

Common misunderstanding:

no-cache does not mean do not store.
no-cache means store may revalidate before reuse.
no-store means do not store.

For sensitive enterprise data:

Cache-Control: no-store

is often safer.


16. Cacheability by API Type

Sensitive quote/order detail

Likely:

Cache-Control: no-store

because it may contain:

- customer data
- pricing
- discount
- contract term
- PII/commercially sensitive data

Public/static metadata

Possibly:

Cache-Control: public, max-age=3600

for non-sensitive static reference data.

Catalog-like data

Catalog data is tricky.

It may be cacheable but must respect:

- tenant
- customer segment
- eligibility
- effective date
- catalog version
- pricing rules

Bad cache key can leak tenant/customer-specific catalog result.

Internal verification:

- Is catalog response tenant-specific?
- Is pricing included?
- Does cache key include tenant/effective date/catalog version?

17. Private vs Public Cache

private means response is for one user/client and should not be stored by shared caches.

Cache-Control: private, max-age=60

public means shared caches may store it.

Cache-Control: public, max-age=3600

For multi-tenant enterprise APIs, defaulting to public cache can be dangerous.

Senior rule:

If response depends on Authorization, tenant, user, customer, account, permission, or contract terms, do not use public shared cache unless design proves safety.

18. Vary Header

Vary tells caches which request headers influence response.

Example:

Vary: Accept-Encoding

If response varies by media type:

Vary: Accept

If response varies by tenant header:

Vary: X-Tenant-ID

But using high-cardinality headers in Vary can reduce cache effectiveness.

Failure mode:

Cache stores JSON response for one Accept/tenant combination and reuses it incorrectly for another.

Internal verification:

- Are there shared caches?
- Which headers affect representation?
- Does gateway/CDN respect Vary?

19. ETag Mental Model

ETag is an entity tag: a version identifier for a representation.

Example response:

HTTP/1.1 200 OK
ETag: "quote-v7"
Content-Type: application/json

Client can revalidate:

GET /quotes/Q-1001 HTTP/1.1
If-None-Match: "quote-v7"

If unchanged:

HTTP/1.1 304 Not Modified

ETag can be used for:

- cache validation
- optimistic concurrency
- lost update prevention

But representation matters.

If JSON response changes because field ordering or derived metadata changes, ETag strategy must be clear.


20. Strong vs Weak ETag

Strong ETag:

ETag: "abc123"

Means representation is byte-for-byte equivalent for strong comparison.

Weak ETag:

ETag: W/"abc123"

Means semantically equivalent but not byte-identical.

For APIs, many teams use version-based ETag:

ETag = hash(resourceVersion + representationVersion)

Potential issue:

If ETag is only DB row version, but response includes computed pricing, permission-sensitive fields, or tenant-specific projection, ETag may be wrong.

Internal verification:

- Is ETag used?
- Is it strong or weak?
- Is it based on DB version, updatedAt, hash, or representation version?

21. Conditional GET with If-None-Match

Flow:

1. Client GETs resource.
2. Server returns ETag.
3. Client sends If-None-Match on later GET.
4. Server returns 304 if unchanged.

Example:

GET /catalogs/current HTTP/1.1
If-None-Match: "catalog-v42"

Response if unchanged:

HTTP/1.1 304 Not Modified
ETag: "catalog-v42"

Benefit:

Reduce payload transfer for large stable resources.

Risk:

Wrong ETag can return stale catalog/pricing data.

22. Conditional Update with If-Match

Flow:

1. Client GETs resource with ETag.
2. Client updates using If-Match.
3. Server updates only if current ETag matches.
4. If mismatch, server returns 412.

Example:

PUT /quotes/Q-1001 HTTP/1.1
If-Match: "quote-v7"
Content-Type: application/json

If current version is v8:

HTTP/1.1 412 Precondition Failed

This prevents lost updates.

Alternative:

{
  "quoteId": "Q-1001",
  "version": 7,
  "changes": {
    "requestedTermMonths": 24
  }
}

Both models are valid if standardized.

Senior review:

For mutable resources, what prevents stale client overwriting newer state?

23. ETag, Authorization, and Tenant Isolation

ETag must not leak cross-tenant information.

Bad design:

Global sequential version appears in ETag and lets caller infer other tenants' updates.

Bad cache design:

Shared cache keys only by path, ignoring Authorization/Tenant.

For tenant-sensitive resource, cache and ETag strategy must include tenant/security boundary.

Internal verification:

- Are tenant-specific responses cacheable?
- Does cache key include tenant/security context?
- Can ETag leak internal versioning or activity?

24. Cache-Control for Error Responses

Error responses can also be cached accidentally.

Example risk:

404 for resource not yet created gets cached.
Later resource is created.
Client still sees cached 404.

For dynamic APIs, consider explicit:

Cache-Control: no-store

on error responses unless there is a deliberate policy.

Rate limit response may use:

HTTP/1.1 429 Too Many Requests
Retry-After: 30
Cache-Control: no-store

Internal verification:

- Are errors cacheable by gateway/proxy?
- Are 404/403 responses cached?
- Does API gateway apply default cache rules?

25. Caching and POST

GET is naturally cache-friendly. POST usually is not cached by default in typical API usage.

But some command-like endpoints are expensive and deterministic.

Example:

POST /pricing/preview

It might be tempting to cache.

Danger:

Pricing depends on tenant, customer, agreement, catalog version, effective date, discount permission, currency, tax boundary.

If caching is used, key must include all correctness dimensions.

Often safer:

- explicit idempotency key
- explicit quote/pricing run resource
- server-side result persistence
- cache only internal pure calculation with robust key

Senior review:

Is this really cache, or should it be a materialized resource/result?

26. JAX-RS Implementation Patterns

Explicit media types

@Path("/quotes")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class QuoteResource {
    ...
}

Class-level annotation sets default for methods.

Method-level annotation can override:

@GET
@Path("/{quoteId}/proposal.pdf")
@Produces("application/pdf")
public Response proposalPdf(@PathParam("quoteId") String quoteId) {
    ...
}

Review question:

Is the override obvious and documented?

Cache-Control in response

CacheControl cache = new CacheControl();
cache.setNoStore(true);

return Response
    .ok(response)
    .cacheControl(cache)
    .build();

For public cache:

CacheControl cache = new CacheControl();
cache.setMaxAge(3600);
cache.setPrivate(false);

return Response
    .ok(response)
    .cacheControl(cache)
    .tag(etag)
    .build();

Conditional request handling

@GET
@Path("/{quoteId}")
public Response getQuote(
        @PathParam("quoteId") String quoteId,
        @Context Request request
) {
    QuoteResponse quote = quoteService.getQuote(quoteId);
    EntityTag etag = new EntityTag(quote.versionTag());

    Response.ResponseBuilder preconditions = request.evaluatePreconditions(etag);
    if (preconditions != null) {
        return preconditions
            .cacheControl(noStoreOrPolicy())
            .build();
    }

    return Response
        .ok(quote)
        .tag(etag)
        .cacheControl(noStoreOrPolicy())
        .build();
}

Note:

This is a pattern. Actual implementation depends on internal framework conventions.

If-Match for update

@PUT
@Path("/{quoteId}")
public Response updateQuote(
        @PathParam("quoteId") String quoteId,
        @HeaderParam("If-Match") String ifMatch,
        UpdateQuoteRequest request
) {
    if (ifMatch == null || ifMatch.isBlank()) {
        return Response.status(428).build(); // if internal policy uses 428
    }

    QuoteResponse updated = quoteService.updateIfVersionMatches(
        quoteId,
        request,
        parseEtag(ifMatch)
    );

    return Response.ok(updated)
        .tag(new EntityTag(updated.versionTag()))
        .build();
}

428 Precondition Required can be used if policy requires conditional update. Verify internal standard before using.


27. Gateway and Proxy Interaction

Application code is not the only actor.

Gateway may:

- terminate TLS
- compress response
- add security headers
- enforce CORS
- cache GET responses
- strip hop-by-hop headers
- add request ID
- normalize status code
- handle OPTIONS
- enforce max body size

Therefore:

A behavior observed in curl against service port may differ from behavior through gateway.

Testing should include both:

- service-level tests
- gateway/integration environment tests

28. Hop-by-Hop vs End-to-End Headers

Some headers are hop-by-hop and should not be forwarded blindly.

Examples:

Connection
Keep-Alive
Transfer-Encoding
Upgrade
Proxy-Authenticate
Proxy-Authorization
TE
Trailer

End-to-end headers describe the message/resource across intermediaries.

Examples:

Content-Type
Cache-Control
ETag
Authorization
traceparent

Senior concern:

Do not build business logic on headers that may be removed or changed by intermediaries.

29. Failure Modes

Failure modeSymptomLikely causeDetection
415 Unsupported Media TypePOST fails before method invokedmissing/wrong Content-Typeaccess log, JAX-RS logs
406 Not Acceptableclient cannot get responseAccept mismatchraw HTTP test
Browser CORS errorUI call blocked, backend may look finepreflight/header policybrowser devtools, OPTIONS logs
Compression breaks streamdelayed SSE/chunksproxy buffering/compressionpacket timing, client trace
Stale quote/catalogclient sees old datawrong Cache-Control/ETagcache logs, response headers
Cross-tenant data leaksevere security incidentcache key ignores tenant/authaudit/security investigation
Lost updateuser overwrites newer statemissing If-Match/version checkDB version conflict analysis
304 when data changedstale cachewrong ETag generationconditional GET tests
Huge payload latencyslow responseno compression/paginationpayload metrics

30. Debugging Workflow

When content negotiation/caching behavior is wrong:

1. Capture raw request and response headers.
2. Confirm path/method matched expected endpoint.
3. Check Content-Type and Accept.
4. Check @Consumes/@Produces.
5. Check MessageBodyReader/Writer registration.
6. Check filters/interceptors.
7. Check gateway/ingress CORS/compression/cache policy.
8. Check client library default headers.
9. Check OpenAPI contract.
10. Reproduce with curl using explicit headers.

Useful curl commands:

curl -i \
  -H 'Accept: application/json' \
  https://example.internal/quotes/Q-1001
curl -i \
  -X POST \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  --data '{"customerId":"C-100"}' \
  https://example.internal/quotes

Preflight simulation:

curl -i \
  -X OPTIONS \
  -H 'Origin: https://portal.example.com' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: authorization,content-type,x-correlation-id' \
  https://example.internal/quotes

Conditional request:

curl -i \
  -H 'If-None-Match: "quote-v7"' \
  https://example.internal/quotes/Q-1001

31. Testing Strategy

Tests should cover protocol behavior, not only Java object behavior.

Minimum endpoint tests:

- correct Content-Type response
- unsupported request Content-Type -> 415
- unsupported Accept -> 406, if runtime policy supports it
- required CORS behavior, if service-owned
- Cache-Control for sensitive response
- ETag presence/absence according to policy
- If-Match stale update -> 412 or internal equivalent
- no-store for error response, if required

Contract tests:

- OpenAPI documents media types
- OpenAPI documents headers
- generated client can call endpoint
- status/header behavior matches contract

Integration environment tests:

- gateway does not strip required headers
- compression behavior matches policy
- CORS works through real entrypoint
- cache headers survive ingress/APIM

32. PR Review Checklist

Review these questions:

Content negotiation
- Are @Consumes/@Produces explicit?
- Are supported media types intentional?
- Does OpenAPI match implementation?
- Are 406/415 cases understood?

CORS
- Is this endpoint browser-accessible?
- Is CORS handled by service or gateway?
- Are allowed origins strict?
- Are credentials handled safely?
- Does preflight work?

Compression
- Is payload large enough to benefit?
- Is compression done by gateway/platform?
- Could compression break streaming/SSE?
- Are binaries excluded?

Caching
- Is response sensitive to user/tenant/customer/authorization?
- Should it be no-store, private, or public?
- Is Vary correct?
- Are error responses cache-safe?

ETag/conditional request
- Is ETag needed?
- Is ETag based on correct representation/version?
- Is If-Match needed to prevent lost update?
- Is stale update handled deterministically?

Operations
- Can headers be observed in logs/traces?
- Are gateway/platform policies documented?
- Are integration tests hitting the real ingress path?

33. Internal Verification Checklist

Verify internally:

API governance
- Are @Consumes/@Produces required by style guide?
- Is JSON the only standard representation?
- Are XML/PDF/CSV endpoints allowed and governed?
- Is media type versioning used?

CORS
- Which layer owns CORS?
- Are origins configured per environment?
- Are authenticated browser APIs allowed?
- Are OPTIONS requests routed to service or gateway?

Compression
- Which layer compresses responses?
- What payload threshold is used?
- Are streaming endpoints excluded?
- Are binary payloads excluded?

Caching
- Are any APIs cacheable?
- Are quote/order/customer/pricing responses forced no-store?
- Are catalog responses cacheable?
- Is tenant included in cache key?
- Does gateway/APIM cache anything by default?

ETag/conditional request
- Is ETag used in existing APIs?
- Is optimistic concurrency exposed through If-Match or body version?
- Are 304/412/428 part of internal status code policy?
- Are ETags representation-specific?

Testing/observability
- Are raw HTTP headers tested?
- Are response headers visible in logs/traces?
- Are gateway header mutations documented?

34. Senior Engineer Heuristics

Use these rules:

If request has body, Content-Type matters.
If response body has format, Content-Type matters.
If client asks for unsupported Accept, expect 406 or governed fallback.
If endpoint is browser-accessible, CORS must be explicitly owned.
If response is sensitive, default to no-store.
If response varies by tenant/user/auth, shared cache is dangerous.
If resource is mutable, consider lost update prevention.
If using ETag, define what representation/version it represents.
If using streaming, verify compression/proxy buffering.
If gateway manages a behavior, test through gateway, not only service port.

35. Key Takeaways

Content negotiation, CORS, compression, caching, and ETag are not optional HTTP trivia.

They control:

- whether request reaches resource method
- whether response can be decoded
- whether browser can call API
- whether large payloads are efficient
- whether stale data can appear
- whether tenant data can leak through cache
- whether lost updates are prevented

A senior engineer should not just ask:

Does the endpoint return the expected JSON?

Ask:

Does the endpoint behave correctly as an HTTP contract across clients, gateways, caches, browsers, retries, and concurrent updates?
Lesson Recap

You just completed lesson 22 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.