Skip to content

Spring Boot & API Interview Preparation

  1. Optimizing a Spring Boot Application
  2. IoC — Inversion of Control
  3. Dependency Injection
  4. Spring Bean Lifecycle
  5. Singleton vs Prototype
  6. Securing REST APIs
  7. Troubleshooting a Slow API
  8. REST vs GraphQL vs gRPC
  9. Senior/Lead Interview Summary

How would you optimize a Spring Boot application handling millions of requests daily?

I would optimize the application across the complete request path rather than focusing only on Spring Boot code.

The first step is to identify the actual bottleneck using metrics, logs, tracing, and profiling.

Monitor:

  • API latency — P50/P95/P99
  • Requests per second
  • CPU and memory utilization
  • GC pauses
  • Thread-pool utilization
  • Database connection-pool usage
  • Database query latency
  • Cache hit/miss ratio
  • External API latency
  • Error and timeout rate

Typical observability stack:

Spring Boot
|
+--> Spring Boot Actuator
|
+--> Micrometer
|
+--> Prometheus
|
+--> Grafana
|
+--> OpenTelemetry / Distributed Tracing

Avoid unnecessary processing.

Keep controllers thin:

@GetMapping("/customers/{id}")
public CustomerDto getCustomer(@PathVariable Long id) {
return customerService.getCustomer(id);
}

Use:

  • Efficient algorithms
  • Pagination
  • DTOs
  • Lazy loading where appropriate
  • Projections
  • Minimal object creation
  • Appropriate data structures

Do not load 100,000 records when the UI needs only 20.

Page<CustomerDto> findCustomers(Pageable pageable);

Database performance is often a major bottleneck.

I would:

  • Add appropriate indexes
  • Analyze slow queries
  • Eliminate N+1 queries
  • Use projections for read-heavy APIs
  • Use pagination
  • Optimize joins
  • Tune connection pooling
  • Consider read replicas
  • Consider partitioning/sharding for very large datasets

Example Hikari configuration:

spring.datasource.hikari.maximum-pool-size=30
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.connection-timeout=3000

The pool size should be determined through load testing rather than simply increasing it.


Use caching for frequently accessed, relatively stable data.

Typical choices include Redis.

flowchart LR
    Client --> API[Spring Boot API]
    API --> Cache[Redis Cache]
    Cache -->|Cache Miss| DB[(Database)]
    DB --> Cache
    Cache --> API
    API --> Client

Example:

@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
return repository.findById(id).orElseThrow();
}

Consider:

  • TTL
  • Cache invalidation
  • Cache size
  • Cache stampede protection
  • Cache consistency

For horizontal scaling, keep services stateless where practical.

flowchart TD
    Client --> LB[Load Balancer]
    LB --> A1[Spring Boot Instance 1]
    LB --> A2[Spring Boot Instance 2]
    LB --> A3[Spring Boot Instance 3]
    A1 --> Redis[(Redis)]
    A2 --> Redis
    A3 --> Redis
    A1 --> DB[(Database)]
    A2 --> DB
    A3 --> DB

This allows instances to be added or removed without requiring sticky sessions.


Tune:

  • Tomcat/Jetty thread pool
  • Async executor
  • Database connection pool
  • HTTP client connection pool
  • Kafka consumer configuration

Do not simply increase the number of threads.

Too many threads can cause:

More Threads
Context Switching
CPU Contention
Higher Latency
Lower Throughput

For I/O-heavy workloads, evaluate Java Virtual Threads when the application and dependencies are suitable.


For external or downstream service calls, use:

  • Timeouts
  • Retry with exponential backoff
  • Circuit breakers
  • Bulkheads
  • Rate limiting
  • Fallbacks where appropriate

Example flow:

flowchart LR
    Request --> Timeout
    Timeout --> Retry
    Retry --> CB[Circuit Breaker]
    CB --> Downstream[Downstream Service]
    CB --> Fallback[Fallback]

Retries should be used carefully, especially for non-idempotent operations.


Move non-critical work out of the synchronous request path.

Example:

flowchart LR
    Client --> API[POST /order]
    API --> Order[Create Order]
    Order --> Kafka[Kafka Event]
    Order --> Response[Return Response]
    Kafka --> Email[Email Consumer]
    Kafka --> Inventory[Inventory Consumer]
    Kafka --> Analytics[Analytics Consumer]

This prevents slow downstream processing from blocking the API request.


Monitor:

  • Heap utilization
  • Allocation rate
  • Old-generation usage
  • GC pause time
  • Memory leaks

Evaluate G1GC or, depending on latency requirements and Java version, ZGC.

Do not simply increase -Xmx without understanding the memory behavior.


For millions of requests, make the application horizontally scalable.

With Kubernetes:

  • Horizontal Pod Autoscaler
  • CPU/memory metrics
  • Custom metrics such as request rate
  • Readiness/liveness probes
  • Rolling deployments

For microservices:

flowchart LR
    Client --> Gateway[API Gateway]
    Gateway --> User[User Service]
    Gateway --> Order[Order Service]
    Gateway --> Payment[Payment Service]

The gateway can handle:

  • Authentication
  • Routing
  • Rate limiting
  • Correlation IDs
  • TLS termination
  • API versioning

Every request should have a correlation or trace ID.

sequenceDiagram
    participant C as Client
    participant G as API Gateway
    participant O as Order Service
    participant P as Payment Service
    participant D as Database

    C->>G: Request traceId=abc123
    G->>O: Request traceId=abc123
    O->>P: Payment request
    P->>D: Query
    D-->>P: Result
    P-->>O: Payment result
    O-->>G: Response
    G-->>C: Response

Collect:

  • Throughput
  • Latency
  • Errors
  • CPU/memory
  • DB pool utilization
  • Request/correlation ID
  • Endpoint
  • HTTP status
  • Error details
  • Business identifiers where appropriate
  • Service-to-service latency
  • Database latency
  • External API latency

Test:

Normal Load
Peak Load
Stress Test
Failure Scenarios
Recovery

Use realistic traffic patterns and validate every performance change.

For a Spring Boot application handling millions of requests daily, I would first identify the bottleneck using metrics, logs and distributed tracing rather than optimizing blindly. At the application level I would optimize expensive operations, use pagination and DTOs, and tune thread pools. At the database level I would optimize queries and indexes, eliminate N+1 problems, use connection pooling and potentially read replicas. For frequently accessed data I would introduce Redis caching with proper TTL and invalidation. I would keep services stateless so they can scale horizontally behind a load balancer or Kubernetes HPA. For downstream services I would use timeouts, retries, circuit breakers and bulkheads, while Kafka can be used for asynchronous processing. Finally, I would validate the architecture with load testing and continuously monitor P95/P99 latency, throughput, errors, JVM/GC, database and cache metrics.

Lead-level phrase:

I optimize based on measured bottlenecks and validate every change through load testing and production observability.


Normally, a Java class creates and manages its own dependencies:

class OrderService {
private PaymentService paymentService = new PaymentService();
}

Here, OrderService controls the creation of PaymentService.

With IoC, Spring takes control of object creation and dependency management.

@Service
class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}

Spring creates the objects and manages their relationships.

flowchart LR
    App[Application Code] --> Container[Spring IoC Container]
    Container --> Order[OrderService Bean]
    Container --> Payment[PaymentService Bean]
    Order --> Payment

IoC means the control of creating and managing objects is transferred from the application code to the Spring IoC container.

Common container interfaces:

  • BeanFactory
  • ApplicationContext

Spring Boot applications commonly use ApplicationContext.


Dependency Injection is the mechanism Spring uses to implement IoC.

Suppose:

class OrderService {
private PaymentService paymentService;
}

OrderService depends on PaymentService.

Instead of creating it:

PaymentService paymentService = new PaymentService();

Spring injects it.

Preferred approach:

@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}

Benefits:

  • Dependencies are explicit
  • Supports immutable fields
  • Easier unit testing
  • Prevents partially initialized objects
  • Makes mandatory dependencies clear

@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}

Useful when a dependency is optional or needs to be changed after construction.


@Autowired
private PaymentService paymentService;

Generally avoided because:

  • Dependencies are hidden
  • Harder to unit test
  • Prevents straightforward immutability
  • Encourages partially initialized objects

Dependency Injection means an object’s dependencies are provided by an external container rather than the object creating those dependencies itself.


A simplified Spring bean lifecycle:

flowchart TD
    A[Application Starts] --> B[Bean Definition]
    B --> C[Bean Instantiation]
    C --> D[Dependency Injection]
    D --> E[Aware Callbacks]
    E --> F[BeanPostProcessor Before Initialization]
    F --> G["@PostConstruct"]
    G --> H["afterPropertiesSet()"]
    H --> I[Custom Init Method]
    I --> J[BeanPostProcessor After Initialization]
    J --> K[Bean Ready]
    K --> L[Application Uses Bean]
    L --> M[Application Shutdown]
    M --> N["@PreDestroy"]
    N --> O["DisposableBean.destroy()"]
    O --> P[Bean Destroyed]

Example:

@Component
public class PaymentService {
public PaymentService() {
System.out.println("Constructor");
}
@PostConstruct
public void init() {
System.out.println("PostConstruct");
}
@PreDestroy
public void destroy() {
System.out.println("PreDestroy");
}
}

Typical sequence:

Constructor
Dependency Injection
@PostConstruct
Bean Available
@PreDestroy

@PostConstruct is useful for initialization after dependencies have been injected.

@PreDestroy is useful for cleanup.


These are Spring bean scopes.

Singleton is the default Spring bean scope.

@Service
public class OrderService {
}

Spring creates one bean instance per Spring IoC container.

flowchart TD
    C[Spring ApplicationContext] --> O[OrderService Singleton]
    A[OrderController] --> O
    B[OrderProcessor] --> O

Spring Singleton does not mean one instance for the entire JVM.

It means:

One instance per Spring ApplicationContext/container.

Singleton beans are normally shared, so avoid mutable request-specific state inside them.


@Component
@Scope("prototype")
public class ReportGenerator {
}

Spring creates a new instance whenever the bean is requested from the container.

flowchart TD
    C[Spring Container] --> R1[ReportGenerator #1]
    C --> R2[ReportGenerator #2]
    C --> R3[ReportGenerator #3]
Feature Singleton Prototype
Default? Yes No
Instances One per container New instance when requested
Shared? Yes No
Suitable for Stateless services Stateful/short-lived objects
Memory usage Lower Higher
Destruction Spring manages lifecycle Spring does not fully manage destruction

This is a common senior interview question.

@Service
public class OrderService {
private final ReportGenerator reportGenerator;
public OrderService(ReportGenerator reportGenerator) {
this.reportGenerator = reportGenerator;
}
}

Where:

@Scope("prototype")
@Component
class ReportGenerator {
}

Even though ReportGenerator is prototype scoped, normal injection into a singleton means one prototype instance is created when the singleton is created and remains referenced by the singleton.

It does not automatically create a new prototype instance every time OrderService is used.

For a fresh prototype instance, use ObjectProvider:

@Service
public class OrderService {
private final ObjectProvider<ReportGenerator> provider;
public OrderService(ObjectProvider<ReportGenerator> provider) {
this.provider = provider;
}
public void process() {
ReportGenerator generator = provider.getObject();
}
}

IoC: Spring takes control of object creation and lifecycle.

Dependency Injection: Spring provides the dependencies required by an object.

Bean Lifecycle: Spring creates, injects, initializes, manages and destroys beans where applicable.

Singleton vs Prototype: Singleton provides one instance per Spring container, while Prototype provides a new instance whenever the bean is requested.


How do you secure REST APIs? Discuss JWT, OAuth2, Refresh Tokens, and Session Management.

A strong approach is to use OAuth2/OIDC with an established identity provider, short-lived access tokens, secure refresh-token handling, Spring Security authorization, and defense-in-depth.

flowchart LR
    U[User] --> C[React / Mobile Client]
    C --> AS[Authorization Server]
    AS --> AT[Access Token]
    AS --> RT[Refresh Token]
    AT --> API[Spring Boot API]
    API --> V[Spring Security JWT Validation]
    V --> AUTH[Authorization]
    AUTH --> RES[Protected Resource]

JWT stands for JSON Web Token.

A JWT is commonly used as an access token.

Typical flow:

User
|
| Login
v
Authorization Server
|
| Access Token
v
Client
|
| Authorization: Bearer <JWT>
v
Spring Boot API
|
| Validate JWT
v
Allow / Reject

Example claims:

{
"sub": "12345",
"iss": "https://auth.example.com",
"aud": "order-api",
"scope": "orders:read orders:write",
"exp": 1780000000
}

Validate:

  • Signature
  • Issuer
  • Audience
  • Expiration
  • Not-before when applicable
  • Required scopes/roles

JWT is signed by default, not encrypted.

Do not put passwords or sensitive confidential data into the payload.


Example:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/orders/**")
.hasAuthority("SCOPE_orders:read")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth ->
oauth.jwt());
return http.build();
}
}

OAuth2 is primarily an authorization framework.

For user authentication and identity, OAuth2 is commonly combined with OpenID Connect (OIDC).

Component Responsibility
Resource Owner User
Client React/mobile application
Authorization Server Authenticates user and issues tokens
Resource Server Spring Boot API
Access Token Grants access to resources

For enterprise systems, use an established identity provider rather than implementing authentication from scratch.


Access tokens should generally be short-lived.

sequenceDiagram
    participant C as Client
    participant AS as Authorization Server
    participant API as Spring Boot API

    C->>AS: Login
    AS-->>C: Access Token + Refresh Token
    C->>API: Bearer Access Token
    API-->>C: API Response
    C->>AS: Refresh Token
    AS-->>C: New Access Token

Refresh tokens allow the client to obtain new access tokens without requiring the user to log in repeatedly.

Consider:

  • Controlled refresh-token lifetime
  • Secure storage
  • Refresh-token rotation
  • Reuse detection
  • Revocation
  • HTTPS
  • Avoid exposing refresh tokens to JavaScript where possible

For browser applications, a common design is to keep refresh credentials in a Secure, HttpOnly, appropriately scoped cookie.


JWT-based APIs can be stateless.

Request
Bearer JWT
Validate Token
Authorize

This supports horizontal scaling:

flowchart TD
    LB[Load Balancer] --> A1[API Instance 1]
    LB --> A2[API Instance 2]
    LB --> A3[API Instance 3]

However, stateless APIs can still have centralized state for:

  • Refresh tokens
  • Revocation
  • User/device sessions
  • Token rotation

For example:

flowchart LR
    A1[API 1] --> R[Redis / Session Store]
    A2[API 2] --> R
    A3[API 3] --> R

Authentication:

Who are you?

Authorization:

What are you allowed to do?

Example:

@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) {
}

Or scope-based authorization:

.requestMatchers("/orders/**")
.hasAuthority("SCOPE_orders:read")

Also enforce resource-level authorization.

A valid JWT does not automatically mean the user can access every resource.

User A
|
+--> Order 100: Allowed
|
+--> Order 200: Forbidden

JWT/OAuth2 alone is not enough.

Always use TLS.

@NotBlank
@Size(max = 100)
@Email

Protect sensitive endpoints from abuse.

Configure allowed origins explicitly.

Avoid unrestricted origins for security-sensitive authenticated applications.

CSRF requirements depend on the authentication mechanism.

Cookie-based authentication requires careful CSRF protection because browsers automatically attach cookies.

Bearer-token APIs that use an Authorization header have a different CSRF risk model.

Never log:

  • Passwords
  • Authorization headers
  • Access tokens
  • Refresh tokens
  • Secrets

Log safe metadata such as:

userId
clientId
endpoint
HTTP status
timestamp
traceId

How would you troubleshoot a slow API? What metrics would you analyze?

I would troubleshoot top-down, starting with the API and drilling into dependencies.

flowchart LR
    Client --> Gateway[API Gateway]
    Gateway --> App[Spring Boot]
    App --> DB[(Database)]
    App --> Redis[(Redis)]
    App --> External[External APIs]

    App --> Trace[Distributed Tracing]
    Trace --> Bottleneck[Identify Bottleneck]

Analyze:

  • P50 latency
  • P95 latency
  • P99 latency
  • Requests per second
  • Error rate
  • Timeout rate
  • Response payload size

P95/P99 are especially important because averages can hide slow requests.


Check:

  • CPU utilization
  • Memory utilization
  • GC pauses
  • Thread-pool utilization
  • Thread dumps
  • Blocked threads
  • Deadlocks

Check:

  • Slow SQL queries
  • Query execution time
  • Missing indexes
  • Connection-pool exhaustion
  • DB CPU
  • Active connections
  • Locks
  • Deadlocks

Check:

  • Downstream API latency
  • Timeouts
  • Retry count
  • HTTP connection-pool utilization
  • Circuit-breaker state

Check:

  • Cache hit ratio
  • Cache miss ratio
  • Redis latency
  • Evictions
  • Memory usage

Check:

  • Network latency
  • Load balancer metrics
  • Pod CPU/memory
  • Kubernetes scaling
  • Container restarts
  • Node resource pressure

Use distributed tracing:

Client
Gateway
Spring Boot
Redis / DB
External Service

The objective is to determine where the latency was introduced.

I first check P95/P99 latency and distributed traces, then drill into application, database, cache, downstream services, and infrastructure metrics to isolate the bottleneck. I then reproduce the issue with load testing and validate the fix with the same metrics.


REST GraphQL gRPC
Best for Public APIs, web/mobile Flexible UI data fetching Internal microservices
Protocol HTTP HTTP HTTP/2
Data format Usually JSON JSON Protocol Buffers
Performance Good Good, with query overhead Very high
Contract OpenAPI commonly used GraphQL schema .proto contract
Browser support Excellent Excellent Limited directly; gRPC-Web may be required
Streaming Limited Supported Excellent
Complexity Low Medium Medium
Typical use External/business APIs Complex frontend Service-to-service

REST is usually the default choice for business and public-facing APIs.

  • Building public APIs
  • Supporting web/mobile clients
  • Implementing CRUD operations
  • Interoperability and simplicity are important
  • APIs are resource-oriented
  • Easy debugging is important

Example:

React / Mobile App
|
HTTPS
|
v
Spring Boot API
|
+-- GET /customers/123
+-- POST /customers
+-- PUT /customers/123
+-- DELETE /customers/123
  • Simple
  • Widely understood
  • Excellent browser/client support
  • Easy to test
  • Strong HTTP ecosystem
  • Good caching support

Can cause:

  • Over-fetching
  • Under-fetching
  • Multiple calls for complex screens

GraphQL is useful when clients need flexible control over the data they retrieve.

  • Frontend screens require multiple resources
  • Different clients need different fields
  • You want to reduce over-fetching
  • You want to reduce under-fetching
  • UI requirements change frequently
  • Frontend needs aggregated data

Example query:

query {
customer(id: "123") {
name
email
orders {
id
amount
}
payments {
status
}
}
}

Architecture:

flowchart LR
    UI[React Application] --> G[GraphQL API]
    G --> C[Customer Service]
    G --> O[Order Service]
    G --> P[Payment Service]
  • Flexible queries
  • Reduces over-fetching
  • Can aggregate multiple services
  • Strong schema and type system
  • Good fit for complex frontend applications
  • More complex implementation
  • More complicated caching
  • Query complexity needs controls
  • N+1 query problems can occur
  • Authorization needs careful design

GraphQL is not automatically faster than REST. Its main advantage is flexible data fetching.


gRPC is well suited for high-performance service-to-service communication.

It commonly uses:

  • HTTP/2
  • Protocol Buffers
  • Strongly typed contracts
  • Binary serialization
  • Streaming
  • Communicating between internal microservices
  • Low latency is important
  • High throughput is required
  • Strong API contracts are important
  • Streaming is required
  • Services are controlled by the same organization

Example:

Order Service
|
gRPC
|
v
Inventory Service
|
gRPC
|
v
Payment Service

Example .proto contract:

service InventoryService {
rpc CheckStock(StockRequest) returns (StockResponse);
}
  • High performance
  • Compact binary serialization
  • Strongly typed contracts
  • Excellent streaming
  • HTTP/2 multiplexing
  • Good fit for internal microservices
  • Less browser-friendly
  • Debugging is less straightforward
  • Requires generated code
  • Adds operational complexity

flowchart TD
    A[Choose API Style] --> B{Public or External API?}

    B -->|Yes| R[REST]
    B -->|No| C{Complex Frontend Data Fetching?}

    C -->|Yes| G[GraphQL]
    C -->|No| D{Internal Service-to-Service?}

    D -->|Yes| E{Low Latency / High Throughput / Streaming?}
    D -->|No| R

    E -->|Yes| GR[gRPC]
    E -->|No| R

Choice: REST

flowchart LR
    Mobile[Mobile App] --> REST[REST API]
    REST --> Spring[Spring Boot]

Why?

  • Simple
  • Easy to consume
  • Excellent HTTP ecosystem
  • Easy documentation

Choice: GraphQL

flowchart LR
    React[React Dashboard] --> GraphQL[GraphQL API]
    GraphQL --> Users[Users]
    GraphQL --> Orders[Orders]
    GraphQL --> Payments[Payments]

Why?

The UI can request exactly the fields required.


Choice: gRPC

flowchart LR
    Order[Order Service] -->|gRPC| Inventory[Inventory Service]
    Order -->|gRPC| Payment[Payment Service]

Why?

  • Low latency
  • Strong contract
  • Efficient serialization
  • Good service-to-service communication

A system can use all three.

flowchart LR
    WEB[React Web App] --> REST[REST API]
    MOBILE[Mobile App] --> REST

    ADMIN[Complex Admin UI] --> GRAPH[GraphQL API]

    REST --> ORDER[Order Service]
    GRAPH --> ORDER

    ORDER -->|gRPC| INVENTORY[Inventory Service]
    ORDER -->|gRPC| PAYMENT[Payment Service]
    ORDER -->|gRPC| CUSTOMER[Customer Service]

Typical strategy:

  • REST → External/public APIs
  • GraphQL → Complex frontend aggregation
  • gRPC → Internal high-performance service communication

I would use REST as the default for public and business-facing APIs because it is simple, interoperable, and easy to consume. I would choose GraphQL when frontend clients need flexible or aggregated data and I want to reduce over-fetching or under-fetching. I would choose gRPC for high-performance internal service-to-service communication where strong contracts, low latency, and streaming are important.


Identify the bottleneck first using P95/P99 latency, metrics, logs and tracing. Optimize the application, database, cache, downstream calls and infrastructure based on evidence. Validate improvements through load testing.

Spring controls object creation and dependency management instead of application code creating dependencies directly.

Dependencies are provided by Spring rather than being created by the consuming class. Constructor injection is generally preferred.

Spring instantiates the bean, injects dependencies, runs initialization callbacks, makes the bean available, and runs destruction callbacks where applicable.

Singleton gives one instance per Spring container. Prototype creates a new instance when requested from the container.

Use OAuth2/OIDC with Spring Security, short-lived JWT access tokens, securely managed refresh tokens, proper authorization, HTTPS, rate limiting, validation, secure logging and appropriate CSRF/CORS controls.

Start with P95/P99 latency and distributed tracing, then investigate application, database, cache, downstream services and infrastructure metrics.

REST is the default for simple/public APIs, GraphQL is useful for flexible frontend data fetching, and gRPC is a strong choice for high-performance internal microservice communication.


Spring Boot Performance
|
+-- Measure first
+-- Optimize DB
+-- Cache
+-- Tune pools
+-- Async processing
+-- Resilience
+-- Horizontal scaling
+-- Observability
Spring Core
|
+-- IoC
+-- Dependency Injection
+-- Bean Lifecycle
+-- Singleton / Prototype
API Security
|
+-- OAuth2 / OIDC
+-- JWT
+-- Refresh Tokens
+-- Authorization
+-- Session Management
+-- HTTPS / CORS / CSRF
+-- Rate Limiting
API Troubleshooting
|
+-- P95 / P99
+-- CPU / Memory / GC
+-- Threads
+-- DB
+-- Cache
+-- Downstream Services
+-- Infrastructure
+-- Distributed Tracing
API Selection
|
+-- REST → Public / simple APIs
+-- GraphQL → Flexible frontend queries
+-- gRPC → Internal high-performance calls