Core and Web-based Java
REST, Testing, Security and Microservices
PGCP-AC
A production API combines several disciplines. HTTP defines the application protocol, tests prove behaviour at selected boundaries, security establishes identity and permission and distributed design accounts for latency and partial failure. Framework annotations help implement these concerns, but they do not decide resource semantics, authorization policy, retry safety or service boundaries.
1. REST as an architectural style
REST describes constraints for networked systems rather than one payload format. Important ideas include resources identified by URIs, representations transferred between client and server, stateless requests, cacheable responses where appropriate, a uniform interface and layered intermediaries.
A resource is a concept such as a book or order, not a controller method:
GET /books/42 retrieve a book
POST /books create a book
PUT /books/42 replace at a known URI
PATCH /books/42 partially modify
DELETE /books/42 remove
JSON is common, but REST does not require JSON. SOAP is a messaging protocol with structured envelopes and related standards; REST is an architectural style using representations and uniform HTTP semantics.
2. Resource and URI design
Use nouns for resources and let the HTTP method express the action. Prefer /orders/17/items to /getOrderItems?id=17. Query parameters commonly express filtering, sorting, searching or pagination:
GET /books?author=Patil&sort=title&page=0&size=20
Stable URIs should avoid exposing database details or implementation class names. Nested paths should express a real containment or access relationship without becoming deeply coupled object navigation.
Some business commands do not fit simple CRUD. A subordinate operation such as POST /orders/17/cancellations can model a cancellation request more clearly than inventing a verb-like GET.
3. HTTP method properties
GET and HEAD are safe: clients do not request a state change. PUT and DELETE are idempotent: repeating the same intended operation has the same state effect. POST is not inherently idempotent.
Idempotence does not mean identical response bytes. Repeating DELETE can return 204 first and 404 later while the resource remains absent. It means repeated application does not multiply the intended state change.
Retries, caches, browser behaviour and gateways rely on method semantics. Never use GET to perform a purchase, deletion or other mutation.
4. Status codes
Use status codes as part of the public contract:
| Situation | Typical status |
|---|---|
| Successful retrieval | 200 OK |
| Successful creation | 201 Created |
| Successful operation with no body | 204 No Content |
| Invalid syntax or binding | 400 Bad Request |
| Missing or invalid authentication | 401 Unauthorized |
| Authenticated but forbidden | 403 Forbidden |
| Resource absent | 404 Not Found |
| State conflict | 409 Conflict |
| Semantic validation failure | 422 Unprocessable Content where adopted |
| Unexpected server failure | 500 Internal Server Error |
A 201 response should normally include a Location for the created resource. Do not return 200 for every outcome with an error flag in JSON; intermediaries and clients depend on HTTP status.
5. Spring request mapping
@RestController
@RequestMapping("/api/books")
class BookController {
@PostMapping
ResponseEntity<BookResponse> create(
@Valid @RequestBody CreateBookRequest request) {
BookResponse created = service.create(request);
URI location = URI.create("/api/books/" + created.id());
return ResponseEntity.created(location).body(created);
}
@GetMapping("/{id}")
BookResponse find(@PathVariable long id) {
return service.require(id);
}
}
@RequestBody uses an HTTP message converter to deserialize content. @PathVariable binds a path segment and @RequestParam binds query or form values. Binding is not authorization and deserialization is not full business validation.
6. Content negotiation and representations
The request Content-Type declares the submitted representation. Accept describes desired response media types. Spring selects message converters using these headers, method declarations and configuration.
Return 415 when the request media type is unsupported and 406 when no acceptable response representation is available. Declare produced and consumed formats intentionally.
API versioning may use paths, media types or other controlled policies. Every strategy has operational costs. Prefer compatible additive changes and publish deprecation and removal rules.
Invoking and inspecting REST services
An API client must construct the method, URI, headers, authentication and body that the server contract expects, then interpret both status and representation. Postman is an interactive client for saving requests, environment variables, headers, bodies and response checks. It is useful for exploration and demonstrations, but a manually successful request is not a replacement for repeatable automated tests.
Traditional Spring applications often use RestTemplate for synchronous HTTP calls. A request may send an HttpEntity containing headers and a body and receive a ResponseEntity containing status, headers and converted content. Configure connection and read timeouts, inspect non-success responses deliberately and publish client-facing DTOs rather than sharing persistence entities. Newer applications may use other Spring HTTP clients, but the boundary rules remain the same: finite timeouts, explicit serialization, safe authentication and predictable error mapping.
ResponseEntity<BookResponse> result = restTemplate.exchange(
serviceUrl + "/books/{id}",
HttpMethod.GET,
HttpEntity.EMPTY,
BookResponse.class,
id);
SOAP and REST clients follow different contracts. SOAP commonly exchanges XML envelopes described through service metadata and operation contracts. REST clients apply HTTP resource and representation semantics. Choosing one does not remove transport failures, authentication needs, schema compatibility or timeout handling.
7. DTOs at API boundaries
A DTO defines the public contract:
record BookResponse(long id, String title, BigDecimal price) {}
Do not expose persistence entities automatically. Entities may contain lazy proxies, bidirectional cycles, internal fields, audit information or relationships that change independently of the API. Direct exposure also lets persistence refactoring break clients.
Use distinct request and response DTOs when their responsibilities differ. Map explicit fields and never allow client input to bind protected state such as owner, role, approval status or calculated price without server policy.
8. Validation and error contracts
Structural constraints belong on request DTOs:
record CreateBookRequest(
@NotBlank String title,
@Positive BigDecimal price) {}
Services enforce business invariants that require state or authorization. Central exception handling should return a stable error representation containing an error type or code, safe message, status, path, field errors where relevant and correlation identifier.
Never expose stack traces, SQL, secret values or internal implementation names. Distinguish malformed JSON, constraint violations, missing resources, conflicts and unexpected failures.
9. Pagination, filtering and concurrency
Collections should be paginated with deterministic ordering. Offset pagination is simple but large offsets can be costly and concurrent inserts can shift pages. Cursor or keyset pagination scales better for large ordered data.
Conditional requests can prevent lost updates. A server returns an ETag and a client submits If-Match during update. If the representation changed, the server rejects the stale write rather than overwriting it silently.
Define maximum page sizes and filter allowlists to protect the database from unbounded work.
10. Unit tests
A unit test exercises a small behaviour with dependencies replaced by fakes or mocks:
@Test
void rejectsTransferWhenBalanceIsInsufficient() {
AccountRepository accounts = new InMemoryAccounts(...);
TransferService service = new TransferService(accounts);
assertThrows(InsufficientFundsException.class,
() -> service.transfer(1, 2, amount));
}
Unit tests are fast and precise. Test observable behaviour, not private methods or every internal call. A fake often describes repository behaviour more clearly than extensive mock expectations.
11. MVC and slice tests
MockMvc exercises Spring MVC mapping, binding, validation, filters, exception handling and response generation without an external live HTTP server:
mockMvc.perform(post("/api/books")
.contentType(MediaType.APPLICATION_JSON)
.content(invalidJson))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors.title").exists());
An MVC slice usually replaces services, so it cannot prove persistence or transaction behaviour. Repository slices test mappings and queries. Slice tests are valuable when their intentionally excluded layers are understood.
12. Integration and end-to-end tests
Integration tests exercise collaborating components and infrastructure. A repository integration test should use the relevant database behaviour; an in-memory substitute may differ in SQL dialect, constraints, transactions or locking.
An API integration test can start the application, send HTTP, commit through the real persistence layer and verify the public representation. End-to-end tests may include deployed services and external boundaries but are slower and harder to diagnose.
Use the smallest boundary that proves the risk. Do not claim database compatibility from a mocked repository or controller routing from a service unit test.
13. Test quality
Good tests are deterministic, isolated, readable and focused on outcomes. Control time through an injected Clock, random values through deterministic sources and external systems through explicit test doubles or managed containers.
Test important failures: invalid input, authorization denial, transaction rollback, duplicate requests, timeouts and dependency errors. Avoid broad sleeps and shared mutable fixtures.
Coverage is evidence of execution, not proof of meaningful assertions. Mutation testing, boundary analysis and defect history can reveal gaps beyond line coverage.
14. Authentication and authorization
Authentication establishes who the caller is. Authorization decides whether that identity may perform a particular operation on a particular resource.
A logged-in user is not automatically allowed to update every book or view every account. Server code must enforce role, permission, tenant and object-ownership policies. A role displayed or hidden by the browser is only presentation; the server remains authoritative.
Default-deny policies and method-level authorization reduce accidental exposure. Error handling should avoid revealing whether a protected resource exists when that information itself is sensitive.
15. Password storage
Passwords should be processed with a password-hashing function designed to be slow and salted, such as a suitably configured adaptive algorithm. Store the encoded hash, algorithm parameters and salt as required by the format.
Never store plaintext passwords or use reversible encryption as the password database. General fast hashes are unsuitable because attackers can test guesses rapidly.
Authentication compares through the password encoder and can upgrade older hashes after successful login. Add rate limits, monitoring, multi-factor options and secure account recovery.
16. Basic and session authentication
HTTP Basic sends a Base64-encoded username and password with requests. Base64 is encoding, not encryption, so HTTPS is mandatory. Repeated credential exposure and limited logout semantics make Basic more suitable for controlled clients than rich browser login.
Session authentication stores server-side login state associated with a secure cookie identifier. Use Secure, HttpOnly and appropriate SameSite settings, rotate the identifier after login, protect state-changing requests against CSRF and invalidate on logout.
17. JWT structure and validation
A compact JWT commonly contains Base64url-encoded header, payload and signature. A signature provides integrity and issuer authentication under the key policy; it does not encrypt payload claims. Anyone holding the token can usually decode them.
Validation must include:
- an allowed algorithm rather than trusting the token header blindly;
- a valid signature with the correct key;
- expected issuer and audience;
- expiry and not-before constraints with controlled clock skew;
- required subject, token type and application claims.
Do not put secrets in an ordinary signed JWT. Short lifetimes reduce exposure; revocation and logout need an explicit design. Authorization still evaluates current server policy rather than trusting every stale role claim indefinitely.
18. Common web threats
API security includes:
- injection prevention through parameterized queries and controlled structure;
- output encoding for cross-site scripting;
- CSRF protection where browser credentials attach automatically;
- strict CORS policy for browser cross-origin access;
- size and type limits for request bodies and uploads;
- mass-assignment prevention with request DTOs;
- secure error handling and logging;
- rate limiting and abuse detection;
- dependency and secret management.
CORS is a browser policy, not server authentication. Allowing an origin does not grant a user permission and blocking an origin does not stop non-browser clients.
19. Microservice boundaries
A microservice owns a cohesive business capability and can be developed and deployed independently. Boundaries should follow domain responsibility and change patterns, not “one table per service.”
Independent deployment requires more than separate repositories. A service needs clear API ownership, data responsibility, release control, operational monitoring and limited coupling. A distributed monolith has separate processes but requires coordinated changes and failures.
Microservices introduce network latency, partial failure, duplicated operational work and distributed consistency. A modular monolith is often the better starting point when independent scaling and ownership are not yet required.
Fragmenting a business requirement means locating cohesive capabilities and assigning clear ownership rather than mechanically splitting every class or database table. A useful boundary keeps rules and data that change together inside one service and exposes a stable contract to other capabilities. Poor fragmentation creates chatty request chains, duplicated rules and releases that still require every service to change together.
Deployment patterns include multiple service processes on hosts, virtual machines or containers, usually with automated configuration, health checks, rolling replacement and independent versioning. Multiple instances improve capacity and availability only when traffic is balanced and state is externalized or deliberately replicated. A service-per-container convention can simplify isolation, but deployment packaging does not by itself create a sound microservice boundary.
20. Data ownership and consistency
Each service should control its data model. Another service calls its contract rather than updating private tables. Shared physical infrastructure does not justify shared schema ownership.
A local database transaction cannot atomically update independent services. Distributed workflows often use:
- events and eventual consistency;
- sagas coordinating local transactions;
- compensating actions that semantically reverse completed steps;
- outbox patterns that atomically store state and an event for later publication.
A compensation is not a database rollback. Refunding a payment is a new auditable business action and may itself fail.
21. API gateways and discovery
An API gateway provides a client-facing entry point for routing, authentication integration, rate limiting, TLS termination, request shaping and selected aggregation. Avoid placing all business logic in the gateway.
Service discovery maps a logical service identity to changing instances. Client-side discovery selects an instance in the caller; server-side discovery delegates selection to a load balancer or gateway.
Discovery tells where an instance is. Health checks, load balancing, timeouts and authorization remain separate responsibilities.
22. Timeouts, retries and idempotency
Every remote call needs a finite timeout budget. Without it, blocked calls consume threads, connections and upstream capacity.
Retry only transient failures, with limits, backoff, jitter and awareness of the total deadline. Retrying a non-idempotent write can duplicate an order or payment when the first attempt succeeded but its response was lost.
Idempotency keys let a server recognize repeated client commands and return the recorded outcome. The server must define key scope, request matching, storage duration, concurrency control and response replay.
23. Resilience patterns
A circuit breaker stops repeated calls to a failing dependency and later probes recovery. A bulkhead isolates capacity so one dependency cannot consume every thread or connection. Rate limiting protects finite resources. Fallbacks must be truthful; invented success can corrupt business state.
Retries multiply load, so retries across several layers can create a storm. Choose one responsible layer and observe retry counts. Resilience patterns supplement capacity planning and failure correction rather than hiding persistent failure.
24. Messaging and delivery semantics
Asynchronous messaging decouples availability and smooths load, but consumers must handle duplicate, delayed and reordered messages.
At-least-once delivery can repeat processing, so consumers need idempotent effects or deduplication. Ordering is usually guaranteed only within a key or partition. A dead-letter destination retains repeatedly failing messages for diagnosis and controlled replay.
Schema evolution should be backward compatible where mixed producer and consumer versions coexist. Include stable event IDs, occurrence time, version and trace context.
25. Observability
Observability combines:
- logs: structured event details with correlation or trace IDs;
- metrics: rates, errors, latency distributions, saturation and business outcomes;
- traces: a request's path across service boundaries.
A running process is not necessarily healthy. Readiness indicates whether an instance should receive traffic; liveness indicates whether it should be restarted. Dependency checks need careful timeout and failure policies.
Monitor user outcomes such as successful checkout, not only CPU and HTTP 200 counts. Never log passwords, access tokens, session IDs or unnecessary personal data.
Practical considerations
| Mistake | Correct approach |
|---|---|
| Verb-based CRUD URLs | Model resources and use HTTP methods |
| Returning 200 for every failure | Use meaningful status codes |
| Returning JPA entities directly | Publish deliberate DTOs |
| Claiming integration from mocked infrastructure | Test the actual boundary |
| Treating authentication as authorization | Check permission per operation |
| Assuming signed JWT means encrypted | Treat payload as readable |
| One table per microservice | Split by cohesive business capability |
| Sharing database tables across services | Give each service data ownership |
| Retrying every write | Require transient classification and idempotency |
| Using process-up as business health | Measure outcomes, dependencies and saturation |
Worked distributed request trace
For an order request:
- The gateway authenticates the token and routes the request.
- Order service authorizes the customer and validates the DTO.
- An idempotency key prevents duplicate order creation.
- One local transaction stores the order and an outbox event.
- A publisher sends the event after commit.
- Payment service processes it idempotently.
- Failure triggers a retry within policy or a compensating workflow.
- Trace context connects logs and timings across services.
No single local transaction covers every step. Durable state, idempotency, messages and compensation provide recoverable progress.
Continue learning
Related notes
Put this topic into timed practice
Open mock tests when you want full-exam pacing, or keep drilling in practice mode.