DEV Community

Said Olano
Said Olano

Posted on

🧱 A Java Architecture Stack, Layer by Layer: From APIs to Availability and Recovery

A Java Architecture Stack, Layer by Layer: From APIs to Availability and Recovery

A production system is rarely one application. It is a stack of concerns that must work together: an API that accepts requests, a database that stores state, identity that decides who may do what, infrastructure that runs the code, pipelines that ship it, and mechanisms that keep it standing when traffic spikes or something breaks. Each concern has a layer, and a weakness in any layer shows up as an outage somewhere else.

This article walks through twelve layers of a typical Java architecture, from APIs and backend logic down to availability and recovery. For each layer we look at the purpose, the Java and Spring Boot tooling that usually fills it, and the decisions that matter most. The goal is a shared checklist you can use in design reviews.

1. APIs and Backend Logic

This is where requests enter the system and business rules run. In Java, the common choice is Spring Boot with Spring Web MVC for blocking workloads or WebFlux for reactive ones. Keep controllers thin, put business rules in services, and return stable DTOs rather than persistence entities.

@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {

    private final PlaceOrderUseCase placeOrder;

    public OrderController(PlaceOrderUseCase placeOrder) {
        this.placeOrder = placeOrder;
    }

    @PostMapping
    public ResponseEntity<OrderResponse> create(@Valid @RequestBody CreateOrderRequest request) {
        Order order = placeOrder.execute(request.toCommand());
        return ResponseEntity.status(HttpStatus.CREATED).body(OrderResponse.from(order));
    }
}
Enter fullscreen mode Exit fullscreen mode

Decisions here include versioning, idempotency for writes, and explicit validation at the boundary.

2. Database and Storage

Choose storage by access pattern, not habit. Relational databases such as PostgreSQL suit transactional data with integrity rules. Spring Data JPA or jOOQ provide the Java access layer. Use migrations with Flyway or Liquibase so schema changes are versioned and repeatable.

public interface OrderRepository extends JpaRepository<OrderEntity, UUID> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("select o from OrderEntity o where o.id = :id")
    Optional<OrderEntity> findForUpdate(@Param("id") UUID id);
}
Enter fullscreen mode Exit fullscreen mode

Plan for connection pooling with HikariCP, indexes that match real queries, and backups you have actually restored at least once.

3. Auth and Permissions

Authentication answers who the caller is. Authorization answers what they may do. In Spring, Spring Security with OAuth2 and JWT is the usual combination. Validate tokens at the edge and enforce fine-grained permissions close to the business operation, not only at the URL level.

@PreAuthorize("hasAuthority('ORDER_CANCEL') and #order.ownerId == authentication.name")
public void cancel(Order order) {
    order.cancel();
}
Enter fullscreen mode Exit fullscreen mode

Keep roles and scopes minimal, and rotate signing keys through a JWKS endpoint.

4. Hosting and Deployment

Decide how the artifact reaches production. A typical path packages the service as a container image with a multi-stage Dockerfile, then deploys it to a runtime such as Kubernetes, a managed container service, or a virtual machine managed with systemd. Configuration belongs in the environment, not in the image. Use readiness and liveness probes so the platform knows when an instance can take traffic.

5. Cloud and Compute

Compute decisions shape cost and resilience. Right-size JVM heaps against container memory limits. Set -XX:MaxRAMPercentage rather than a fixed heap so the JVM adapts to its environment. Choose between instance types, serverless functions, and managed containers according to workload shape, and prefer horizontal scaling for stateless services.

6. CI/CD and Version Control

Every change should flow through the same automated path: commit, build, test, scan, package, deploy, verify. Git provides history and review. Jenkins, GitHub Actions, or GitLab CI runs the pipeline. Gate merges on unit and integration tests, and gate releases on contract tests and a smoke test against the deployed environment.

Keep pipelines reproducible by pinning JDK and dependency versions, and treat the pipeline definition itself as code under review.

7. Security and Row-Level Security

Defense in depth means no single control is trusted alone. Validate inputs, encode outputs, keep secrets out of source control, and enforce data access at the database when the platform supports it. PostgreSQL row-level security (RLS) lets the database filter rows by tenant or owner, so a missed where clause in application code does not expose another customer's data.

ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON orders
    USING (tenant_id = current_setting('app.tenant_id')::uuid);
Enter fullscreen mode Exit fullscreen mode

In Spring, set the session variable per request so the policy applies to every query.

8. Rate Limiting

Rate limits protect shared resources from abuse and accidental overload. Apply them at the edge with nginx, an API gateway, or a managed firewall, and add per-user or per-key limits inside the application when business rules require them. Return 429 Too Many Requests with a Retry-After header so well-behaved clients back off.

Libraries such as Bucket4j implement token-bucket limits in Java when you need them inside the service.

9. Caching and CDN

Caching removes repeated work. A CDN serves static assets and cacheable responses close to users. Inside the service, Spring Cache with Caffeine or Redis reduces database load for hot reads. Every cache needs an explicit invalidation strategy and a time to live. A cache without invalidation is a bug waiting for a customer to find it.

@Cacheable(cacheNames = "products", key = "#id", unless = "#result == null")
public Product findProduct(String id) {
    return productRepository.findById(id).orElse(null);
}
Enter fullscreen mode Exit fullscreen mode

10. Load Balancing and Scaling

Distribute traffic across instances with a load balancer or an nginx upstream block. Choose a strategy such as least connections when request durations vary. Scale stateless services horizontally, and keep session state in a shared store or in signed tokens. Scale stateful components, such as databases, with replicas, partitioning, or managed services, not by adding application instances.

11. Error Tracking and Logs

When something fails, you need to know quickly and understand why. Log structured JSON with a correlation ID on every request, and send exceptions to an error tracker such as Sentry or a centralized logging stack. Avoid logging secrets or personal data. Alert on symptoms customers feel, such as error rate and latency at the percentile level, rather than on every exception.

12. Availability and Recovery

Availability is a design target, not a feature. Define service level objectives for each customer-facing path, then design failure behavior to meet them. Use timeouts, retries with backoff, and circuit breakers through Resilience4j so one slow dependency does not exhaust threads. Run regular backups and restore drills, and document a recovery runbook with a measured recovery time objective.

@CircuitBreaker(name = "paymentService", fallbackMethod = "queuePayment")
public PaymentResult charge(PaymentRequest request) {
    return paymentClient.charge(request);
}
Enter fullscreen mode Exit fullscreen mode

Using the Stack as a Checklist

You do not need every layer on day one. A small internal service might need only layers one through four, plus logs. A customer-facing platform needs all twelve, with the most attention on security, rate limits, and recovery. Walk through the stack in every architecture review and ask, for each layer: what is the decision, who owns it, and how would we know if it failed?

Common Mistakes

  • Skipping the layers you find boring. Backups, logs, and rollbacks decide how bad an incident becomes.
  • Enforcing security in one place only. Edge checks, application checks, and database policies each catch different mistakes.
  • Caching without invalidation. Stale data is harder to debug than slow data.
  • Retrying without limits. Unbounded retries turn a partial outage into a full one.
  • Treating the pipeline as an afterthought. A deployment you cannot repeat is a deployment you cannot trust.

Best Practices Checklist

  • Keep controllers thin and DTOs stable.
  • Version the schema and test restores of backups.
  • Separate authentication from authorization, and enforce both.
  • Build once, configure per environment, and probe health.
  • Size the JVM for its container and scale stateless services out.
  • Gate every change through the same automated pipeline.
  • Use defense in depth, including database-level isolation where possible.
  • Limit request rates and cache only with explicit invalidation.
  • Log structured events with correlation IDs and alert on customer symptoms.
  • Define service level objectives and practice recovery.

Conclusion

Architecture is the sum of its layers. Each one answers a question the business will eventually ask: can it handle requests, keep data safe, let the right people in, run reliably, grow under load, ship changes safely, and recover when things break. Java and Spring Boot provide mature tools for every layer. The discipline is in deciding what each layer must guarantee, and in checking that the guarantees hold before the next incident checks for you.

Key takeaways

  • Treat the stack as twelve concerns that must all be designed, not as twelve tools to install.
  • Enforce security and limits at more than one layer.
  • Cache, retry, and scale only with explicit limits and invalidation.
  • Practice recovery as often as you practice deployment.

Top comments (0)