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

Resource and Provider Lifecycle

Lifecycle resource/provider JAX-RS dan dampaknya pada state, concurrency, initialization, shutdown, request scope, dan production failure mode

7 min read1392 words
PrevNext
Lesson 38112 lesson track22–61 Build Core
#jax-rs#lifecycle#provider#resource+4 more

Part 038 — Resource and Provider Lifecycle

Fokus part ini: memahami kapan resource/provider dibuat, dipakai, dibagikan, dan dihancurkan. Banyak bug production bukan berasal dari annotation yang salah, tetapi dari asumsi lifecycle yang salah.

JAX-RS code terlihat seperti class Java biasa.

@Path("/quotes")
public class QuoteResource {
    @GET
    @Path("/{id}")
    public QuoteResponse getQuote(@PathParam("id") String id) {
        // handle request
    }
}

Tetapi class ini tidak hidup seperti class biasa jika ia dikelola container.

Pertanyaan lifecycle:

- siapa membuat instance resource?
- apakah satu instance dipakai semua request?
- apakah instance baru dibuat per request?
- kapan injection terjadi?
- apakah provider/filter/mapper singleton?
- apakah state di field aman?
- kapan resource mahal dibuat dan ditutup?
- apa yang terjadi saat shutdown/rolling deployment?

1. Lifecycle Is a Contract

Lifecycle menentukan safety.

same class + different lifecycle = different correctness properties

Contoh:

public class QuoteResource {
    private QuoteCommand lastCommand;
}

Jika resource per-request, field ini mungkin hanya buruk secara design. Jika resource singleton, field ini bisa menjadi data race dan cross-request leak.

Senior rule:

Never place mutable request state in a field unless you can prove lifecycle and thread-safety.

2. Main Object Categories in JAX-RS Service

Object typeExampleTypical lifecycle concern
resource classQuoteResourceper-request vs singleton, injected deps
providerJSON provider, custom bindersingleton/shared behavior
filterauth/logging/correlation filterthread-safety, ordering, context
interceptorreader/writer interceptorstream handling, shared state
exception mappermaps exception to responsestateless mapping, observability
application configApplication, ResourceConfigstartup registration
DI serviceQuoteServicescope/thread-safety
external clientHTTP/Kafka/DB/Redispool lifecycle, shutdown

Do not treat all of them as ordinary POJOs.


3. Request Lifecycle Overview

Simplified JAX-RS request lifecycle:

sequenceDiagram participant C as Client participant R as Runtime/Container participant F as Filters participant M as Matching participant P as Entity Providers participant Res as Resource Method participant E as ExceptionMapper participant W as Writer C->>R: HTTP request R->>F: request filters F->>M: resource matching M->>P: parameter/entity binding P->>Res: invoke resource method alt exception Res->>E: map exception E->>W: response entity else success Res->>W: response entity end W->>F: response filters F->>C: HTTP response

Lifecycle objects participate at different points.

Resource method runs once per matched request.
Provider/filter/mapper may be reused across many requests.

4. Resource Class Lifecycle

JAX-RS implementations commonly support per-request resource instances, and may support singleton resources depending on registration/configuration.

Do not assume blindly.

Possible models:

ModelMeaningConsequence
per-request resourcenew instance per requestfield state request-local, but still avoid careless mutation
singleton resourceone instance sharedall fields must be thread-safe/immutable
DI-managed scoped resourcescope defined by HK2/CDI/containerbehavior depends on runtime integration
manually registered instanceyou pass object instance to runtimeusually singleton-like

Conceptual per-request:

request 1 -> QuoteResource instance A
request 2 -> QuoteResource instance B

Conceptual singleton:

request 1 -> QuoteResource instance A
request 2 -> QuoteResource instance A
request 3 -> QuoteResource instance A

Internal verification is mandatory.


5. Resource Field State

Resource classes should usually be thin and stateless.

Good:

@Path("/quotes")
public class QuoteResource {
    private final QuoteService quoteService;

    @Inject
    public QuoteResource(QuoteService quoteService) {
        this.quoteService = quoteService;
    }
}

Potentially bad:

@Path("/quotes")
public class QuoteResource {
    private QuoteCommand currentCommand;
    private String currentTenantId;
}

Even if per-request works today, this pattern is fragile:

- lifecycle may change
- async continuation may expose race
- tests may instantiate singleton
- future refactor may register instance manually

Rule:

Keep request data in local variables, method arguments, immutable command objects, or verified request-scoped context.

6. Provider Lifecycle

Providers are extension objects.

Examples:

- MessageBodyReader
- MessageBodyWriter
- ContainerRequestFilter
- ContainerResponseFilter
- ReaderInterceptor
- WriterInterceptor
- ExceptionMapper

Providers are commonly singleton/shared objects. Implementation details vary, but a safe assumption for provider code is:

Provider instances may be reused concurrently.

Therefore provider fields should be:

- immutable configuration
- thread-safe dependencies
- stateless helpers

Unsafe provider:

public class AuditFilter implements ContainerRequestFilter {
    private String currentUser;

    @Override
    public void filter(ContainerRequestContext ctx) {
        currentUser = ctx.getHeaderString("X-User");
    }
}

Safe provider:

public class AuditFilter implements ContainerRequestFilter {
    private final AuditLogger auditLogger;

    @Override
    public void filter(ContainerRequestContext ctx) {
        String user = ctx.getHeaderString("X-User");
        auditLogger.logRequest(user, ctx.getUriInfo().getPath());
    }
}

7. ExceptionMapper Lifecycle

Exception mappers should be stateless translators.

public class DomainExceptionMapper implements ExceptionMapper<DomainException> {
    @Override
    public Response toResponse(DomainException ex) {
        return Response.status(422)
            .entity(ErrorResponse.from(ex))
            .build();
    }
}

Good mapper responsibilities:

- choose status code
- build error payload
- attach correlation ID if available
- log at correct level if mapper owns logging decision
- avoid leaking internal details

Bad mapper responsibilities:

- perform DB lookup
- call external service
- mutate domain state
- store last exception in field
- implement business recovery

Mappers are part of error boundary, not domain workflow.


8. Filter Lifecycle

Filters often handle cross-cutting behavior:

- authentication
- authorization
- correlation ID
- tenant resolution
- logging
- metrics/tracing
- CORS
- request normalization

Filter lifecycle risks:

- shared mutable state
- incorrect ordering
- context not cleared
- request body consumed too early
- response modified after commit
- missing async propagation

Filter ordering should be explicit if multiple filters interact.

Example order dependency:

correlation filter -> auth filter -> tenant filter -> request logging filter

If logging runs before correlation ID creation, logs may be untraceable.


9. Interceptor Lifecycle

Reader/writer interceptors sit near entity stream processing.

Common use cases:

- compression/decompression
- encryption/decryption
- payload logging with limits
- signature verification
- stream wrapping

Risk:

Interceptors can accidentally buffer huge payloads.

Bad:

Read entire request body into memory for logging.

Better:

- log size-limited preview
- log checksum/content length
- log metadata instead of full body
- avoid logging PII/secrets

Interceptors must be treated as production-critical I/O code.


10. Application and ResourceConfig Lifecycle

Application/ResourceConfig belongs to bootstrap lifecycle.

Typical responsibilities:

- register resource classes
- register providers
- register filters/interceptors
- register features
- register DI binders
- set properties
- configure packages/scanning

Do not put request logic here.

Bad:

ResourceConfig constructor performs expensive remote calls without timeout.

Better:

- build config object
- validate critical settings
- register components
- defer optional dependency health to health checks/readiness

Startup should be deterministic and observable.


11. Initialization: Constructor vs PostConstruct vs Lazy

Initialization can happen in multiple places.

PlaceUse forAvoid
constructorassign dependencies, cheap validationexpensive I/O
post construct callbackcontainer-aware initializationlong blocking work without timeout
lazy initializationoptional expensive dependencyrace-prone lazy code
health/readiness checkdependency availability signalmutating global state

Constructor should not surprise operators.

Bad:

public PricingClient(PricingConfig config) {
    this.client = buildClient(config);
    this.client.callHealthEndpoint(); // remote I/O during construction
}

Better:

- validate URI/timeout config in constructor
- check remote availability in readiness or startup validation with explicit timeout/policy

12. Shutdown Lifecycle

Production services must stop cleanly.

Shutdown should:

- stop accepting new work
- let in-flight requests finish within grace period
- stop scheduled jobs
- stop Kafka consumers cleanly
- flush producers if required
- close HTTP clients
- close DB pools
- release Redis clients
- stop custom executors
- flush telemetry/logs if supported

Common leak:

Custom ExecutorService created in singleton but never shut down.

Example:

public class ExportService implements AutoCloseable {
    private final ExecutorService executor;

    @Override
    public void close() {
        executor.shutdown();
    }
}

But implementing AutoCloseable is not enough. The container/bootstrap must actually call it.

Internal verification required:

Which lifecycle callback is honored by the actual runtime?

13. Resource Ownership

Every expensive resource needs a clear owner.

ResourceOwner should beClosed when
DB connection poolapplication singletonapplication shutdown
DB connectionrequest/transaction methodafter operation/transaction
HTTP client poolapplication singletonapplication shutdown
Kafka producerapplication singletonapplication shutdown
Kafka consumerworker lifecycleworker shutdown
executorowning service/componentapplication shutdown
file streammethod/request scopeafter stream completion
temp fileupload/download componentafter processing/failure

Bad smell:

Resource is created in random method and no one knows who closes it.

14. Request Context Lifecycle

Request context exists only while request is active.

Examples:

- URI info
- headers
- security context
- request-scoped beans
- request properties
- MDC values

Lifecycle issue:

executor.submit(() -> {
    // tries to read request context here
});

The background task may run after request context is gone.

Better:

TenantId tenantId = requestContext.tenantId();
CorrelationId correlationId = requestContext.correlationId();

executor.submit(() -> {
    process(command, tenantId, correlationId);
});

Capture immutable values, not framework context object.


15. ThreadLocal and MDC Lifecycle

ThreadLocal can appear in:

- logging MDC
- security context
- tenant context
- tracing context
- transaction context

Thread pools reuse threads.

Therefore every ThreadLocal set must be cleared.

request A sets tenant = T1
thread returns to pool
request B reuses same thread
if not cleared -> request B may see T1

This is a severe tenant/security bug.

Safe filter pattern:

try {
    setContext(ctx);
    proceed();
} finally {
    clearContext();
}

Async/executor propagation requires explicit wrapper or OpenTelemetry/context propagation support.


16. Lifecycle and Async Boundaries

Async boundaries include:

- ExecutorService
- CompletableFuture
- reactive streams
- scheduled jobs
- Kafka consumers
- event listeners
- callbacks

Lifecycle changes at async boundary:

HTTP request scope may not exist.
Servlet request may be completed.
Transaction may be closed.
MDC may be missing.
Security context may not propagate.

Rule:

Async work must receive explicit immutable context.

Example context payload:

public record WorkContext(
    TenantId tenantId,
    UserId initiatedBy,
    CorrelationId correlationId,
    Instant requestedAt
) {}

This is safer than relying on container request state.


17. Lifecycle and Transactions

Database transaction lifecycle must be shorter than or equal to the operation needing consistency.

Dangerous:

Open transaction -> call slow external service -> continue transaction

Risks:

- lock held too long
- connection held too long
- deadlock probability increases
- pool exhaustion
- timeout cascade

Better:

- validate input
- load minimal state
- commit local state
- publish outbox/event
- call external system outside DB lock where possible

Lifecycle is not only object lifecycle; it is also resource/transaction lifecycle.


18. Lifecycle and External Clients

HTTP clients, Kafka producers, Redis clients, and SDK clients often manage pools/buffers.

They should usually be long-lived and reused.

Bad:

public QuoteResponse getQuote(String id) {
    Client client = ClientBuilder.newClient();
    return client.target(...).request().get(QuoteResponse.class);
}

This can leak sockets and destroy performance.

Better:

Create client once at application startup.
Reuse across requests.
Close at shutdown.

But validate thread-safety of the specific library.

Internal verification:

- Is the client documented as thread-safe?
- Where is it created?
- Where is it closed?
- What pool settings exist?

19. Lifecycle and ObjectMapper/Serialization Providers

Serialization providers may use shared mapper/config objects.

Safe if:

- mapper is fully configured before concurrent use
- modules are registered at startup
- date/time/currency rules are stable
- custom serializers are stateless/thread-safe

Dangerous:

- mutating ObjectMapper config at request time
- serializer storing last processed object
- per-tenant serialization config stored in singleton mutable field

If tenant-specific serialization is needed, use explicit strategy/provider keyed by tenant/config.


20. Lifecycle and Feature Flags

Feature flag clients may be:

- local config snapshot
- polling client
- streaming client
- remote evaluation client

Lifecycle questions:

- when does flag data load?
- what is default if provider unavailable?
- does client spawn background threads?
- how is it closed?
- how are stale flags cleaned up?

Feature flag failure mode can be serious:

flag provider unavailable -> all requests fail

For most flags, safe default/degraded mode is preferred.


21. Lifecycle and Multi-Tenancy

Tenant-aware lifecycle mistakes are expensive.

Bad:

@Singleton
public class TenantConfigHolder {
    private TenantConfig currentTenantConfig;
}

Better:

public interface TenantConfigProvider {
    TenantConfig get(TenantId tenantId);
}

Tenant state should be:

- resolved per request/message/job
- passed explicitly
- cached safely by tenant key if needed
- never stored as current tenant in singleton mutable field

Review any field named:

currentTenant
currentUser
currentRequest
lastRequest
activeQuote

These are often lifecycle bugs.


22. Provider Ordering Lifecycle

Providers can depend on execution order.

Example:

1. correlation filter creates correlation ID
2. auth filter validates token
3. tenant filter resolves tenant
4. authorization filter checks permission
5. logging filter records request outcome

If order changes:

- auth logs lack correlation ID
- tenant cannot be resolved from principal
- authorization runs before authentication
- error response lacks security/audit context

Use explicit priority/order where runtime supports it.

Internal verification:

- Which ordering annotation/config is used?
- Are priorities documented?
- Are tests covering order-sensitive behavior?

23. Startup Failure vs First-Request Failure

Some lifecycle failures appear at startup.

- missing provider binding
- invalid config
- classloading error
- ambiguous injection
- malformed ResourceConfig

Others appear only on first request.

- lazy provider missing
- endpoint not registered
- media type provider missing
- request-scoped object unavailable
- external client not initialized

Senior goal:

Critical misconfiguration should fail before traffic.

But not everything should block startup.

Balance:

fail fast for invalid deployment
serve degraded for optional dependency outage

24. Graceful Shutdown in Kubernetes

Lifecycle must align with Kubernetes termination.

Simplified sequence:

1. pod receives SIGTERM
2. readiness should become false
3. load balancer stops sending new traffic after propagation delay
4. in-flight requests continue until grace period
5. app stops workers/consumers
6. app closes pools/clients
7. process exits

If shutdown is wrong:

- request terminated mid-flight
- Kafka message processed but offset not committed
- DB transaction aborted unexpectedly
- file stream interrupted
- duplicate processing after restart

JAX-RS runtime, Servlet container, embedded server, and Kubernetes settings must agree.


25. Lifecycle Diagrams for Review

For every significant component, draw this:

flowchart LR A[Created] --> B[Injected] B --> C[Initialized] C --> D[Used Concurrently] D --> E[Stopped] E --> F[Closed]

Then annotate:

- who creates it?
- who injects it?
- is it shared?
- is it thread-safe?
- who stops it?
- what happens if stop fails?

This simple diagram catches many production bugs.


26. Common Failure Modes

FailureSymptomLikely lifecycle mistake
cross-request data leakwrong user/tenant in log/datamutable singleton or ThreadLocal not cleared
random concurrency bugintermittent incorrect resultshared non-thread-safe state
connection leakpool exhaustionclient/stream/connection not closed
memory leakheap grows over timesingleton cache/list/map unbounded
startup failservice cannot bootbinding/config/provider registration error
first request fail500 after deploylazy missing dependency/provider
graceful shutdown failduplicate/lost workconsumers/jobs not stopped correctly
missing correlation IDuntraceable logsfilter order or context propagation issue
OOM during uploadpod killedmultipart/stream buffered in memory
stale tenant configwrong behavior after config changecache invalidation/lifecycle issue

27. Debugging Lifecycle Issues

Ask in order:

1. Is this object created by JAX-RS, Jersey/HK2, CDI, Servlet, or manually?
2. Is it singleton, request-scoped, per-lookup, or unknown?
3. Is it reused concurrently?
4. Does it have mutable fields?
5. Does it store request/user/tenant state?
6. Does it own external resources?
7. Where are those resources closed?
8. Does it cross async/thread boundary?
9. Does it depend on ThreadLocal/MDC/security context?
10. Does behavior change under load or only after deploy/shutdown?

Evidence:

- ResourceConfig/Application registration
- DI binder/producers
- scope annotations
- provider registration
- logs at startup/shutdown
- thread dump
- heap dump
- connection pool metrics
- Kubernetes pod termination events
- load balancer/gateway logs

28. PR Review Checklist

[ ] Is the resource/provider lifecycle known?
[ ] Are resource classes stateless or safely scoped?
[ ] Are provider/filter/mapper classes stateless or thread-safe?
[ ] Are mutable fields justified and protected?
[ ] Is request/user/tenant state kept out of singleton fields?
[ ] Are expensive resources created once and reused safely?
[ ] Are expensive resources closed during shutdown?
[ ] Are streams/files closed on success and failure?
[ ] Are custom executors shut down?
[ ] Are ThreadLocal/MDC values cleared in finally blocks?
[ ] Is async work given explicit immutable context?
[ ] Are provider/filter ordering requirements explicit?
[ ] Are startup validations clear and bounded by timeout?
[ ] Does graceful shutdown preserve in-flight work semantics?

29. Internal Verification Checklist

For CSG/internal codebase, verify:

[ ] Are JAX-RS resources per-request, singleton, CDI-scoped, or manually registered?
[ ] Are providers singleton/shared by default in the runtime used?
[ ] How are filters/interceptors ordered?
[ ] Which lifecycle callbacks are honored: `@PostConstruct`, `@PreDestroy`, `AutoCloseable`, Servlet lifecycle, custom bootstrap?
[ ] Where are HTTP clients created and closed?
[ ] Where are DataSource/connection pools created and closed?
[ ] Are Kafka producers/consumers application-managed or framework-managed?
[ ] Are custom executors present?
[ ] Are request contexts ThreadLocal, request-scoped bean, explicit object, or header-only?
[ ] Are tenant/user/correlation contexts cleared reliably?
[ ] How does app respond to SIGTERM in container/Kubernetes?
[ ] Is readiness changed before shutdown?
[ ] Are in-flight requests allowed to complete?
[ ] Are scheduled jobs and consumers stopped before process exit?
[ ] Are lifecycle assumptions documented in engineering handbook/onboarding?

30. Senior Engineer Heuristics

1. Lifecycle determines whether field state is safe.
2. Provider/filter/mapper code must assume concurrent reuse unless proven otherwise.
3. Framework context should not be carried into async work.
4. Expensive clients/pools should have one clear owner and one clear close path.
5. Startup should fail for invalid deployment, not optional transient dependency outage.
6. Graceful shutdown is part of correctness, especially for event/job processing.
7. ThreadLocal must be paired with reliable cleanup.
8. Tenant/user/request state should be explicit value, not ambient global state.
9. Registration style can change lifecycle semantics.
10. Verify lifecycle from runtime evidence, not from framework folklore.

31. Practical Exercise

Pick one resource, one filter, one exception mapper, and one external client wrapper.

For each, answer:

- Who creates it?
- How many instances exist?
- Is it shared across requests?
- Does it have mutable fields?
- Does it own resource that must be closed?
- Does it use ThreadLocal/MDC/security context?
- What happens during shutdown?
- What metrics/logs prove lifecycle behavior?

Then classify each object:

ObjectScopeShared?Mutable state?Cleanup ownerRisk
QuoteResourceunknown until verified??runtime?
AuthFilterunknown until verified??runtime?
DomainExceptionMapperunknown until verified??runtime?
PricingHttpClientapp singleton expectedyesconfig/poolapp bootstrapconnection leak

The exercise is complete only when each ? is replaced by evidence from codebase/runtime.

Lesson Recap

You just completed lesson 38 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.