Object-Oriented Programming and Design Questions
The object-oriented paradigm: classes and objects, encapsulation, inheritance, polymorphism, and composition, plus SOLID and other design principles that keep object models maintainable. Covers modeling a domain in objects and defending those design choices. A staple conceptual interview across most engineering roles.
A customer needs ACID-like guarantees across multiple microservices for critical flows, but you cannot change underlying databases. How would you design application-level patterns (sagas, compensating transactions, two-phase commit emulation) to provide stronger consistency guarantees? Discuss trade-offs in complexity, latency, and how your design can be extended as new services and flows are added.
Sample Answer
Requirements (clarify):
- Strong "ACID-like" guarantees for critical flows spanning services (atomicity + isolation + durability desirable; strict serializability likely impossible without DB changes).
- Cannot change underlying DBs; must work at application layer.
- Low-to-moderate additional latency acceptable for critical flows; extensible to new services.
High-level approach:
- Use a hybrid of orchestrated Sagas with optional two-phase-commit (2PC) emulation for highest-criticality flows. Provide a transaction coordinator service, per-service transaction adapters, durable event/log store, and a compensator library.
Core components:
- Transaction Coordinator (TC): orchestrates steps, tracks state in durable store (append-only, immutable log) and enforces timeouts/retries.
- Service Adapters: each microservice implements idempotent APIs, a prepare/commit/compensate interface, and writes an event to local outbox.
- Outbox + Reliable Delivery: local transactional outbox pattern per service to ensure TC sees state changes.
- Compensator Library: standardizes compensating actions and retry/backoff.
- Visibility/Monitoring: dashboards and manual-reconcile tools.
Patterns:
- Orchestrated Saga (default): TC invokes service actions sequentially/parallel, records success; on failure, triggers compensating actions in reverse order. Pros: simpler, lower latency than global locks. Cons: eventual consistency; need compensations.
- 2PC Emulation (for strict flows): implement application-level prepare/commit where prepare causes service to persist a “prepared” state and lock resources (optimistic or pessimistic), commit flips to committed. Use coordinator to gather prepare ACKs, then commit or rollback. Pros: stronger atomicity; Cons: higher latency, risk of long-held locks and blocking, complex timeout/recovery, depends on services supporting prepare semantics and durable prepared state.
- Idempotency + Outbox: ensure retries safe; transaction log durable to recover coordinator after crash.
Data flow example:
- Client requests multi-service transaction to TC.
- TC issues prepare (if using 2PC emulation) or action call (saga) to each Service Adapter.
- Each service writes local change + outbox event in same DB txn and returns prepared/ok.
- TC commits: sends commit or triggers compensations.
Trade-offs:
- Complexity: 2PC emulation significantly increases per-service complexity (locks, prepared state) and operational burden. Orchestrated sagas are easier to adopt incrementally.
- Latency: 2PC adds roundtrips and locking windows -> higher latency. Sagas have lower average latency but visible eventual consistency window.
- Availability: 2PC reduces availability under partitions; as CAP dictates, cannot have perfect consistency + availability. Sagas favor availability.
Extensibility:
- Design TC and Service Adapter interfaces as versioned contracts (prepare/commit/compensate/status). New services implement the adapter and register capabilities.
- Use feature flags to route flows to Saga vs. 2PC emulation per flow.
- Use a schema for logged steps to enable automated compensator generation and human-in-the-loop reconciliation.
- Provide client libraries and templates (idempotency keys, outbox patterns) to reduce onboarding cost.
Operational considerations:
- Strong monitoring, alerting for stuck prepares, compensations failures, and reconciliation UI.
- Define SLAs for max prepare time and compensation retry policies.
- Maintain audit trail for compliance.
Recommendation:
- Default to orchestrated Sagas + outbox + idempotency for most flows; reserve 2PC emulation for a small set of ultra-critical flows after cost/benefit analysis. This balances complexity, latency, and extensibility while meeting near-ACID guarantees where truly needed.
Evaluate two approaches for global distributed rate-limiting for a client-facing API: (a) centralized global counters stored in a strongly-consistent store; (b) local per-region token buckets with periodic reconciliation. Discuss accuracy, latency, operational complexity, failure modes, and how you'd evolve the approach to support new policies (per-IP, per-account, per-endpoint). Which would you recommend and why?
Sample Answer
Clarify goals/constraints first: expected global QPS, acceptable throttle latency, burst handling, tolerance for slight overage, multi-region traffic distribution, SLA for correctness, and cost constraints. With that, compare the two approaches.
Approach A — centralized global counters (strongly consistent store)
- Accuracy: Very high — single source of truth; enforces hard global limits with minimal overshoot.
- Latency: Higher per-request latency (network hop + consensus) unless you colocate or use fast in-memory consensus (e.g., Redis Cluster with strong consistency, or Spanner). Adds tail latency and dependency on WAN RTT.
- Operational complexity: Heavy — maintain strongly-consistent datastore, leader election, cross-region replication, careful capacity planning to avoid being the bottleneck.
- Failure modes: Store outage causes global denial of service unless you implement degraded local-mode; partitioning risks either blocking all requests or allowing uncoordinated overuse.
- Evolution: Easy to support new dimensions (per-IP/account/endpoint) by sharding keys or secondary indices; but high-cardinality policies increase storage/throughput and require sharding/aggregation logic.
Approach B — local per-region token buckets + periodic reconciliation
- Accuracy: Best-effort. Low steady-state overshoot if reconciliation is frequent and tokens allocated conservatively; still allows transient overages during network partitions or before reconciliation.
- Latency: Low — local decision, minimal tail latency, good for user experience and bursty traffic.
- Operational complexity: Moderate — implement token allocation algorithm (static quotas or dynamic leasing), reconciliation and conflict resolution logic, monitoring and heuristics to tune allocations.
- Failure modes: Regions can exceed global quota under partition or misallocation; reconciliation may be complex. You must prevent starvation and ensure fairness across noisy regions.
- Evolution: More complex for high-cardinality policies; per-IP across regions requires either routing affinity or global coordination (counter leases), increasing complexity.
Recommendation
- For client-facing APIs where user experience demands low latency and slight, temporary overage is acceptable, prefer hybrid: local token buckets for low-latency enforcement + a lightweight global coordination layer for long-term guarantees. Implement conservative local quotas (e.g., each region gets a fractional lease), fast local checks, and a global counter to drain/adjust allocations asynchronously. Add a fast fail-path: if global store is healthy, enforce hard limits for high-risk policies (e.g., per-account billing-sensitive); otherwise operate in degraded local mode with monitoring and alerting.
- For strict legal/compliance limits or billing-critical quotas where any overage is unacceptable, use centralized strong counters with caching and careful latency mitigation (edge caching, proximity deployments, circuit-breaker fallbacks).
Practical steps to evolve policies
- Model rate limits as policy objects: {dimension: IP|account|endpoint, window, limit, priority}.
- Use hierarchical keys: global:account:endpoint for precise counters; allow region subkeys for local buckets.
- For high-cardinality (per-IP), prefer probabilistic structures (count-min sketches) + local short windows with global aggregation to reduce storage.
- Implement dynamic lease allocator: periodically rebalance regional quotas based on observed traffic and SLOs.
- Add feature flags to toggle strict vs best-effort enforcement per policy.
Trade-offs summary
- Centralized = correctness, cost/latency; Local = low latency, complexity in correctness. Hybrid gives best practical balance: low-latency UX while maintaining near-global guarantees and operational resilience.
Design a plugin framework that enables business users to add validation and transformation rules for items (e.g., automated price checks, category mappings) without redeploying core services. Describe plugin lifecycle, sandboxing or capability restrictions, versioning, testing and CI integration, performance isolation, and governance/auditability concerns.
Sample Answer
Requirements / goals:
- Allow business users to add/modify validation & transformation rules without redeploying core services
- Secure, versioned, testable, auditable, and performant at scale
- Minimal developer intervention; governed self-service
High-level architecture:
- Core services expose a Plugin Host (gRPC/HTTP) that loads rules from a centralized Plugin Registry (artifact store + metadata DB).
- Business rules authored via a low-code UI or as code (WASM/JavaScript/DSL) and submitted to Registry.
- Runtime executes plugins in sandboxes per tenant or logical scope and composes results into pipelines.
Plugin lifecycle:
- Author (UI or repo) → 2. Validate (static checks, schema) → 3. Test (unit+integration in CI) → 4. Review & approve (governance workflow) → 5. Publish (Registry) → 6. Deploy/Activate (canary/blue-green) → 7. Monitor → 8. Retire.
Sandboxing & capability restrictions:
- Run plugins as WASM modules or in Node isolates with limited host APIs. Provide minimal, explicit capabilities (I/O, DB, network) via capability tokens.
- Enforce resource quotas, timeouts, memory limits, and restrict syscalls.
- Use policy layer (OPA) to enforce approval-level constraints (e.g., no external network access unless approved).
Versioning:
- Immutable plugin artifacts with semantic versioning stored in Registry.
- Runtime supports side-by-side versions per tenant and multilayer precedence rules (global < tenant < environment).
- Support rollbacks and canary routing by percentage.
Testing and CI integration:
- CI pipeline automatically runs static analysis, unit tests, contract tests against a plugin harness that simulates host APIs, security scanners, and performance benchmarks.
- Provide a staging environment that runs real sample payloads. PR gating enforces test coverage and policy checks.
Performance isolation:
- Execute plugins in separate worker pools or serverless containers with concurrency limits.
- Cache deterministic transformations, employ batching, and circuit breakers per plugin.
- Provide per-plugin SLA and throttle or degrade to default rules when overloaded.
Governance & auditability:
- Registry stores audit logs for author, approvals, activations, and runtime invocations.
- Immutable change history, digital signatures for artifacts, and role-based access control for publishing and activation.
- Monitoring: metrics (latency, errors), tracing (distributed spans), and alerting. Regular periodic reviews and automated policy compliance scans.
Trade-offs:
- WASM gives strong isolation and performance but increases dev ramp; DSL/low-code increases business agility but needs richer validation.
- Recommend hybrid: low-code for noncritical rules, WASM for complex logic; strong CI and governance to balance agility and safety.
Describe strategies to make API and event-schema changes backward compatible. Include examples of safe additive changes, deprecation patterns, contract tests, schema registries, and CI gates you'd implement to reduce risk of breaking clients while enabling iterative extension.
Sample Answer
Start with principles: preserve existing contracts (requests, responses, event shapes) clients rely on; prefer additive, non-breaking changes; make incompatible changes explicit, versioned, and timeboxed.
Safe additive changes (examples):
- Add optional fields: introduce "phoneNumber" nullable/optional in JSON/Avro; consumers ignore unknown fields.
- Add new event types or endpoints: POST /v2/orders while keeping /v1/orders live.
- Add optional enum values only if consumers validate unknown values gracefully (or provide a fallback).
Deprecation pattern:
- Mark fields/endpoints deprecated in schema/Swagger with migration guidance and timeline.
- Emit deprecation warnings in responses/metrics/logs.
- Provide migration SDKs and a clear sunset date; maintain a compatibility mode if needed.
Contracts and schema registry:
- Use a schema registry (Avro/Protobuf/JSON Schema) as source of truth to validate producers/consumers and store versions.
- Adopt Consumer-Driven Contracts (e.g., Pact) so consumers publish expectations; providers verify them automatically.
- Enforce backward/forward compatibility rules in the registry (e.g., Avro compatible: BACKWARD or FULL).
CI/CD gates and tests:
- Run contract tests in CI: fail a PR if it breaks any consumer contract.
- Validate schema compatibility on push — reject incompatible changes unless explicitly versioned.
- Run provider integration tests against mock consumer contracts.
- Automate canary/blue-green deploys and run shadow traffic to detect runtime issues.
- Add monitoring and alerting for client errors after deploy and a quick rollback path.
Operational extras:
- Use semantic versioning for API surface; prefer header-based versioning or resource path with explicit version.
- Publish changelogs and migration guides.
- For events, ensure consumers can ignore unknown fields; include event version metadata.
Combining schema registry + consumer-driven contracts + CI gates + gradual rollout minimizes breakage while allowing iterative evolution.
Design a throttling and QoS system for a multi-tenant platform where tenants have different SLAs (free, pro, enterprise). Include enforcement mechanisms (hard vs soft quotas), noisy-neighbor isolation, scaling of enforcement, metering and billing hooks, and how to add new SLA tiers or custom tenant policies later without large refactors.
Sample Answer
Requirements (clarify):
- Functional: per-tenant rate limits, concurrency limits, burst/steady behavior, priority for enterprise, metering for billing, ability to add tiers/custom policies without heavy refactor.
- Non-functional: low-latency enforcement, highly available, horizontally scalable, auditability, per-tenant isolation.
High-level architecture:
API Gateway / Edge → Rate-limiter Service (policy engine + token store) → Traffic Router → Application Workers → Metrics & Billing pipeline → Admin UI & Policy store
Core components:
- Policy Store: versioned, hierarchical policies (global defaults → tier → tenant override). Stored in config DB (e.g., DynamoDB/Postgres) with feature flags.
- Policy Engine: evaluates policy for request (hard vs soft quotas, priorities, weights). Stateless; reads cached policies.
- Enforcement Layer:
- Fast-path local in-memory token bucket counters (per instance) for low-latency checks.
- Global enforcement backed by distributed counters (Redis/Clustered memcached or CRDTs) for absolute hard limits.
- Soft quotas emit warnings/throttles probabilistically (e.g., leaky bucket + 429s with Retry-After header) and trigger prioritization rather than hard block.
- Noisy-neighbor isolation:
- Weighted fair-queueing at gateway based on tenant weight.
- Per-tenant circuit-breakers: if tenant’s error/latency or resource use crosses thresholds, degrade to soft mode or isolate to a rate-limited worker pool.
- Auto-scaling tenant-shards: move heavy tenants to dedicated isolated pools/workers.
- Metering & Billing:
- Every enforced decision emits events to a streaming pipeline (Kafka) with attribution (tenant, SKU, metered units, timestamp).
- Downstream billing service aggregates, applies pricing rules from Policy Store.
- Scaling of enforcement:
- Local caches + probabilistic sampling reduce global counter pressure.
- Sharded counters by tenant hash; dynamic shard rebalancer.
- Use Leases for counter ownership, fallback to best-effort during partition.
- Adding tiers/custom policies:
- Policy DSL (JSON/YAML) + interpreter in Policy Engine. New rules added as new policy entries; no code changes.
- Feature toggles and A/B rollout for new behaviors.
- Tenant-level overrides stored separately; admin UI for policy composition.
Trade-offs:
- Strong global hard limits require consistent distributed counters (higher latency/complexity) vs eventual-consistent soft limits which are faster.
- CRDT/Redis approach balances availability but can allow short bursts during partitions; mitigate with conservative local throttles.
Observability & Ops:
- Per-tenant dashboards, alerting on noisy-neighbor signals, audit logs for enforcement decisions, test harness to simulate policies before rollout.
This design gives low-latency enforcement, clear separation between fast local checks and authoritative global limits, mechanisms to isolate noisy tenants, and an extensible policy model that supports new SLAs and custom tenant policies without major refactors.
Unlock Full Question Bank
Get access to all 40 Object-Oriented Programming and Design interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.