When not to use microservices, avoiding a distributed monolith, domain-driven design (bounded contexts, aggregates), hexagonal (ports and adapters) architecture, internal vs external API design, API gateway anti-patterns, Istio/service mesh vs API gateway, the sidecar pattern, polyglot persistence, team structure (Conway's law, team topologies), multi-tenant application design, and consumer-driven contracts.
Published September 25, 2026
Architecture questions at this level are about boundaries:
The best answers show restraint: the simplest architecture that meets the need, with a clear path to evolve.
Short answer:
The alternative: a modular monolith (Spring Modulith, with module boundaries enforced by ArchUnit and events between modules). It gives most of the design benefits, with one deployment unit.
Learn it in depth → Monolith to Microservices Decomposition
Short answer: A distributed monolith has all the costs of distribution, and none of the independence: services that must be deployed together, share a database, make long synchronous call chains, or share internal libraries with domain logic. Avoid it by:
If services always change together, merge them.
Short answer: DDD is an approach to software design that models the software on the business domain, built with domain experts through a shared ubiquitous language.
Money, Address);Order aggregate with its lines, where the total must match the lines);Learn it in depth → Domain Decomposition
Short answer: Alistair Cockburn's pattern puts the application core (domain plus use cases) at the centre. The core talks to the outside world only through ports:
OrderRepository, PaymentGateway and EventPublisher, implemented by adapters (JPA, a Stripe client, Kafka).Dependencies point inward: the core has no framework imports. The benefits:
It's closely related to Onion and Clean Architecture. In Spring: packages or modules like domain, application (ports and use cases), adapter.in.web, adapter.out.persistence, and config (wiring), enforced with ArchUnit. Pragmatism: for simple CRUD services, a full hexagon adds mapping boilerplate, so apply it where the domain logic is rich.
// application (core)
public interface PlaceOrderUseCase { OrderId place(PlaceOrderCommand cmd); } // inbound port
public interface PaymentPort { PaymentResult authorize(Money amount, String token); } // outbound port
@Service @RequiredArgsConstructor
class PlaceOrderService implements PlaceOrderUseCase {
private final OrderRepositoryPort orders; private final PaymentPort payments;
public OrderId place(PlaceOrderCommand cmd) { /* domain logic only, no HTTP or JPA types */ return null; }
}
// adapter.out.payment
@Component class StripePaymentAdapter implements PaymentPort { /* Stripe SDK calls */ public PaymentResult authorize(Money m, String t) { return null; } }
Short answer:
Short answer:
Short answer: They solve different traffic directions, and overlap a little:
An API gateway: north-south traffic (external clients → the system). It handles client-facing concerns: authentication with user tokens, API keys, rate limiting and quotas, request and response transformation, aggregation, a developer portal, and monetisation.
A service mesh (Istio, Linkerd): east-west traffic (service → service). Transparent, platform-level features for every call:
No application code changes are needed.
Use both: the gateway at the edge (Istio even provides an ingress gateway, and the Kubernetes Gateway API unifies the configuration), and the mesh inside. Don't duplicate retry policies at both layers (they multiply).
Learn it in depth → Design Service Mesh Basics
Short answer: Deploy a helper container alongside the application container in the same pod. They share the network namespace (localhost) and volumes, so the helper adds cross-cutting capabilities without changing the application code. Examples:
The benefits: language-agnostic reuse, a separate release cycle, and isolation. The costs: extra resources per pod, startup ordering (Kubernetes native sidecar containers, restartPolicy: Always init containers, fix this), more complex debugging, and latency hops. Ambient mesh or node-level agents reduce the per-pod overhead.
Short answer: Using different storage technologies for different data needs, often per service or bounded context, instead of one database for everything:
The benefits: each workload gets a fitting data model and scaling characteristics. The costs:
Keep it justified: start with one strong general-purpose database (Postgres goes far: JSONB, full-text search, pgvector), and add specialised stores when a measured need appears.
Short answer:
Short answer:
Tenant identification: resolve the tenant from the authentication token claims (preferred), or the subdomain or header. Propagate a tenant context (a ScopedValue or request-scoped bean, plus MDC and trace attributes, plus message headers).
The data isolation model (per requirement):
tenant_id (cheapest, and needs strict filtering: Hibernate @TenantId/filters, and Postgres row-level security as defence in depth);Spring or Hibernate multi-tenancy uses CurrentTenantIdentifierResolver and MultiTenantConnectionProvider.
Isolation beyond the data:
Operations: tenant onboarding and offboarding automation (provisioning, migrations per schema or database), per-tenant metrics and billing, data residency (region pinning), and backup and restore per tenant.
Testing: automated tests proving that cross-tenant access is impossible.
Learn it in depth → Database Scaling Decision Framework
Short answer: Consumers define the expectations (the requests they send, and the response fields they rely on) as contracts. Providers verify that they satisfy all their consumers' contracts in CI:
can-i-deploy checks the compatibility before release.The benefits: providers know exactly what consumers use, so they can evolve safely; it catches breaking changes before deployment; and it's faster and more reliable than end-to-end environments. It works for messaging too (message pacts). The limitation: it only covers what the consumers wrote down. It complements schema checks and a few end-to-end tests.
Q: What is an anti-corruption layer? A: A translation boundary (adapters, translators, facades) that protects your bounded context's model from an external or legacy system's model, so its concepts and quirks don't leak into your domain.
Q: What is Spring Modulith? A: A Spring project for building modular monoliths: it verifies module boundaries (application modules follow package structure), supports asynchronous application events between modules (with an event publication registry, an outbox-like mechanism), generates documentation, and supports module-scoped integration tests.
Q: How big should a microservice be? A: Big enough to own a cohesive business capability and its data, small enough for one team to understand and change quickly. Size by boundaries and ownership, not by lines of code.
Q: What's the difference between orchestration and choreography, from a team perspective? A: Orchestration centralises the workflow knowledge (one team owns it, and it's easier to see), but it couples the participants to the orchestrator. Choreography distributes the knowledge (more autonomy), but the end-to-end flow is harder to reason about and monitor.