linkedin-1, linkedin-2); a π― Asked in Interview ΓN badge marks questions from direct interview experience, where N is the number of times it recurred. Answers are reproduced in full β nothing truncated.
A monolithic application is built as a single, tightly coupled application where all modules share the same codebase and are deployed together.
A microservices architecture divides the application into small, independent services, each service is responsible for a specific business capability and capable of being developed, deployed, maintained and scaled independently.
Microservices communicate with each other mainly in two ways:
1. Synchronous communication using REST APIs or gRPC (Google Remote Procedure Call), where One service sends a request and waits for a response.
2. Asynchronous communication using message brokers like Kafka or RabbitMQ, where One service sends a message or event, and the other processes it later.
The choice depends on the business requirement.
Ideally, No.
Each microservice should have its own database so that it can be developed, deployed, and scaled independently.
If one service needs data from another service, it should communicate through APIs or events instead of directly accessing another service's database.
For example, in the Candidate Registration Portal, the Payment Service has its own database, and the Booking Service has a separate database.
Data consistency means ensuring that related data across multiple microservices remains accurate and synchronized.
For example, in the Candidate Registration Portal, if the Payment Service marks a payment as successful, the Booking Service should confirm the booking, and the Notification Service should send a success message.
All services should reflect the correct business state.
Since each microservice has its own database, maintaining data consistency requires coordination between services.
This is typically achieved using asynchronous events, the Saga Pattern, retries, and compensation (rollback) actions instead of traditional distributed transactions.
S β Saga Pattern
E β Event-Driven Communication
R β Retry
C β Compensation
Synchronous communication is a communication pattern where one microservice sends a request to another microservice and waits for an immediate response before continuing.
Request-response communication is a communication pattern where one application or microservice sends a request to another service and waits for a response.
It is commonly implemented using REST APIs or gRPC.
For example, in the Candidate Registration Portal, the Payment Service calls the Booking Service to book a seat and waits for the booking confirmation before completing the process.
Asynchronous communication is a communication pattern where one microservice sends a message or event to another microservice without waiting for an immediate response.
For example, in the Candidate Registration Portal, after a successful payment, the Payment Service publishes a PaymentCompleted event. The Notification Service receives the event and sends the confirmation email and SMS in the background, while the Payment Service immediately returns a success response to the user.
API versioning is the practice of maintaining multiple versions of an API so that existing clients continue to work while new features or changes are introduced.
It helps ensure backward compatibility and allows applications to upgrade gradually.
Common API Versioning Strategies :
1. URL Versioning: /api/v1/candidates , /api/v2/candidates
2. Header Versioning: API-Version: 1 , API-Version: 2
3. Query Parameter Versioning: /api/candidates?version=1 , /api/candidates?version=2
I would handle API versioning by creating a new version of the API instead of modifying the existing one.
This allows existing clients to continue using the old version while new clients use the updated version. The most common approach is URL versioning, such as /api/v1 and /api/v2.
I prefer asynchronous communication when an immediate response is not required.
It is suitable for background tasks such as sending notifications, generating reports, processing files, and handling business events.
For example, in the Candidate Registration Portal, after a successful payment, the Payment Service publishes a "Payment Completed" event. The Booking, Notification, and Audit services process the event independently without blocking the Payment Service. This improves scalability, fault tolerance, and loose coupling.
RabbitMQ is a distributed message broker used for asynchronous communication between microservices.
It is commonly used to send, receive, and route messages between applications or microservices.
For example, after a candidate completes payment, the Payment Service sends a "Payment Completed" message to RabbitMQ, and the Booking, Notification, Audit, and Reporting services process the message independently.
This improves scalability, fault tolerance, and loose coupling.
Idempotency means that sending the same API request multiple times produces the same result as sending it once. (One request or repeated requests β Same final result)
It is commonly used in payment and booking systems to prevent duplicate processing caused by retries, network failures, or multiple clicks.
For example, if a candidate retries a payment request due to a network issue, an idempotent API ensures only one payment is created by using an Idempotency Key, preventing duplicate transactions.
For breaking changes, I would introduce a new API version, such as /api/v2, while keeping /api/v1 backward compatible. I would document v2, notify consumers, support both versions during a migration period, monitor V1 usage, and deprecate and eventually remove v1 after clients have migrated.
I would use the Aggregator Pattern. (CompletableFuture, WebClient, RestClient)
I would call the four external APIs, preferably in parallel to reduce latency, collect their responses, and combine them into a single response DTO. I would also use timeouts and fallback handling for APIs that fail or respond slowly.
4 APIs β Parallel calls β Aggregate β 1 response
Service Discovery is a mechanism that helps microservices find and communicate with each other automatically.
Instead of using hardcoded IPs, services register themselves in a registry, and other services find them from the registry when needed.
For example, in the Candidate Registration Portal, the Payment Service can find the Booking Service through Service Discovery even if the Booking Service's IP address changes after scaling or redeployment.
Service Discovery is needed because, in a microservices environment, service instances are frequently created, removed, or restarted. Their IP addresses can change, so hardcoding service locations is not practical. Service Discovery allows services to find each other dynamically, making the system more scalable and reliable.
An API Gateway is a single entry point for all client requests in a microservices architecture.
It receives requests from clients, performs responsibilities such as authentication, authorization, routing, rate limiting, logging, monitoring, and load balancing, and then forwards the requests to the appropriate microservice.
This improves security, scalability, and simplifies communication between clients and microservices.
Dynamic Service Discovery is the process where microservices automatically discover the current IP address and port of other services through a Service Registry like Eureka.
For example, the Booking Service asks the Service Registry for the Payment Service's location, and the registry returns the latest IP address and port.
This eliminates hardcoded URLs and supports scalability and fault tolerance.
Config Server is a centralized configuration management service used in microservices to store and provide configuration properties for multiple services.
I use a centralized Config Server, such as Spring Cloud Config, backed by a Git repository. Config Server is a separate Spring Boot application.
For example, in a Candidate Registration Portal, Payment, Booking, and Notification services act as Config Clients and fetch their common and environment-specific configuration, such as database URLs, service URLs, timeouts, and feature settings.
This avoids duplicate configuration and provides centralized management and version control. Sensitive credentials are stored in a dedicated secrets-management system.
Git β stores configuration
Config Server β provides configuration
Microservices β consume configuration
Eureka clients periodically send heartbeat renewals to the Eureka Server to indicate that they are alive.
If Eureka detects a significant drop in renewals, it enters self-preservation mode and temporarily avoids evicting service instances, because the missing heartbeats could be caused by a network partition rather than actual service failures.
Heartbeat = I am alive.
Self-Preservation = Don't remove everyone just because heartbeats suddenly stopped.
Eureka is a service registry used for service discovery in Spring Cloud microservices.
When a service starts, it registers its name, host, port, and availability information with Eureka.
Other services can then discover available instances dynamically instead of using hardcoded IP addresses.
This is useful because microservice instances can scale, restart, or change their network locations.
In Kubernetes-based environments, however, Kubernetes Service and DNS are commonly used instead of Eureka.
The Circuit Breaker is a fault tolerance pattern that prevents a microservice from repeatedly calling another service that is failing or unavailable.
When the number of failures exceeds a predefined threshold, the circuit breaker opens and temporarily blocks further requests. This prevents cascading failures, improves system stability, and allows the failing service time to recover.
For example, in a Candidate Registration Portal, if the Booking Service repeatedly fails to contact the Payment Service, the circuit breaker opens and temporarily blocks further requests.
Instead of waiting for repeated timeouts, it immediately returns an error or fallback response.
After a configured wait time, it enters the Half-Open state to test whether the Payment Service has recovered.
This prevents cascading failures and improves system stability.
A cascading failure occurs when the failure of one microservice spreads to other dependent services.
For example, if the Payment Service becomes unavailable, the Booking Service may keep waiting or retrying requests, consuming threads and resources.
This can cause the Booking Service and other dependent services to slow down or fail.
We can prevent cascading failures using timeouts, controlled retries, circuit breakers, fallbacks, and bulkhead isolation.
I would use Resilience4j patterns such as Timeout, Circuit Breaker, Retry, and Fallback.
I would first set a timeout so my service doesn't wait indefinitely.
For temporary failures, I would retry a limited number of times.
If the Payment Service continues failing, the Circuit Breaker would open and stop further calls temporarily.
I would then use a fallback response or handle the operation asynchronously, depending on the business requirement.
I would store the client-generated idempotency key, such as a UUID, in Redis or a database along with the request status and result.
On a retry, I check the key first and return the existing result instead of processing again.
For concurrent requests, I use an atomic operation or unique database constraint so only the first request is processed.
Idempotency Key β Redis/DB β Atomic check β Process once β Return same result
No, the application should not necessarily fail when Redis is down.
I would use a fallback based on the use caseβfor example, if Redis is used as a cache, the application can bypass Redis and fetch data directly from the database.
I would also use a timeout and Circuit Breaker to prevent repeated Redis calls from affecting the application.
Redis down β Timeout/Circuit Breaker β Fallback β DB / alternative source
Resilience4j TimeLimiter is used to limit the execution time of an asynchronous operation.
For example, if my Payment Service calls the Booking Service and I configure a 3-second timeout, if the Booking Service doesn't respond within 3 seconds, the operation is timed out, and I can handle it using a fallback or other resilience mechanisms.
Resilience4j is a lightweight Java library used to make microservices more resilient when other services fail or become slow.
It provides patterns like:
Circuit Breaker β Stops calling a failing service.
Retry β Try another request.
Rate Limiter β Limits the number of requests.
Time Limiter β Stops waiting after a timeout.
Bulkhead β Limits concurrent calls to prevent resource exhaustion.
Fallback β Return an alternative response when the call cannot succeed.
The Saga Pattern is a design pattern used to manage distributed transactions in a microservices architecture.
Saga: Each service commits its local transaction; if something fails, compensating transactions undo previous steps β maintain business consistency , Better scalability, eventual consistency.
For example, in a Candidate Registration Portal, if the Payment Service fails after the Booking Service has reserved a seat, the Booking Service cancels the seat reservation.
This avoids using a global transaction while maintaining eventual consistency.
In a microservices architecture, I would avoid a distributed database transaction.
I would use the Saga pattern, where a business transaction is divided into local transactions in each service.
If one step fails, compensating actions are triggered to maintain business consistency.
2PC: All services commit together or all roll back β Strong consistency, higher overhead.
Saga: Each service commits its local transaction; if something fails, compensating transactions undo previous steps β Better scalability, eventual consistency.
Transactional Outbox Pattern ensures reliable database updates and event publishing.
Instead of directly publishing an event to Kafka in the same business flow, I save the business data and the event in an Outbox table within the same database transaction. A separate publisher then reads the Outbox and publishes the event to Kafka.
If Kafka is temporarily unavailable, the event remains in the Outbox and can be retried.
Save Payment + Save Event β Same transaction β Later publish Event to Kafka.
Authentication is the process of verifying the identity of a user before granting access to an application, ensuring that the user is who they claim to be.
User enters username and password -> System validates the credentials -> If valid, the user is authenticated.
For example, in the Candidate Registration Portal, when a candidate enters a username and password, the Authentication Service verifies the credentials. If they are valid, the user is authenticated and allowed to access the portal.
Authentications Different Methods:
1. Session-Based Authentication
2. JWT Authentication
3. OAuth2 Authentication
4. LDAP Authentication
5. Basic Authentication
6. OTP Authentication
Authorization is the process of determining what resources or actions an authenticated user is allowed to access/perform.
For example, in the Candidate Registration Portal, after a user logs in, the system checks their role. An Admin can manage users, while a Candidate can only register for exams and view certificates. If a Candidate tries to access an Admin-only feature, the system denies access.
In JWT (JSON Web Token) authentication, a user logs in with valid credentials, and the server generates a signed JWT token. The client sends this token in the Authorization header with each request. Spring Security validates the token, extracts user details and roles, and authenticates the request without maintaining server-side sessions.
User Login β Validate Credentials β Generate Signed JWT β Client sends "Authorization: Bearer <JWT>" β Validates Token β Extract User & Roles β Authorize Request
To implement JWT in Spring Boot, I authenticate the user using Spring Security, generate a JWT token upon successful login, create a JWT filter to validate tokens for incoming requests, and configure Spring Security to use stateless authentication with JWT tokens instead of sessions.
Rate limiting is a mechanism that restricts the number of requests a client can make to an API within a specific time period.
For example, an API Gateway may allow 100 requests per minute per client and reject additional requests with HTTP 429. (Too Many Requests)
It protects microservices from overload, abuse, and excessive traffic, improving system availability and stability.
To secure communication between microservices and external clients, I would implement authentication, authorization, encrypted communication, secure API access, and centralized security controls.
Security layers:
OAuth2/OIDC + JWT β Client authentication
API Gateway β Central security controls
mTLS β Secure service-to-service communication
RBAC β Authorization
HTTPS/TLS β Encryption in transit
Secrets Manager β Protect credentials/secrets
Database isolation + least privilege β Protect service data
Audit logging β Track security-sensitive actions
Authentication β OAuth2 / OIDC / JWT
Authorization β RBAC
API Gateway β Central security
Services β Defense in Depth
Service-to-Service β mTLS
Principles β Zero Trust + Least Privilege
Exactly-once processing is difficult in distributed systems.
I use a unique event ID and idempotent processing. Before processing a message, I check whether the ID was already processed. If yes, I skip it; otherwise, I process it and save the ID.
For Kafka-to-Kafka communication, Kafka transactions can provide exactly-once semantics.
A Kafka topic is divided into partitions, and each partition is an ordered sequence of messages identified by offsets.
The producer determines the partition, typically using the message key.
Consumers in a consumer group are assigned partitions, allowing messages from different partitions to be processed in parallel while maintaining order within each partition.
Kafka guarantees message ordering only within a partition. If ordering is required for a particular customer, I use Customer ID as the message key so all events for that customer go to the same partition.
Kafka does not guarantee ordering across different partitions.
A DLQ/DLT stores messages that repeatedly fail after configured retries.
It prevents infinite retries and allows messages to be investigated or reprocessed later.
In Kafka, this is commonly implemented as a Dead Letter Topic (DLT).
Retry β Still fails βDLT/DLQ β Investigate β Fix β Reprocess
If a Kafka consumer processes the message successfully but crashes before committing the offset, Kafka considers the message uncommitted and can deliver it again when the consumer restarts. This can cause duplicate processing, so we use idempotent consumer logic to safely handle duplicate messages.
Process β Crash before commit β Redelivery β Possible duplicate β Idempotency
Kafka handles multiple customers and orders by publishing each order as a separate event.
Use Customer ID as the Kafka message key. All orders for the same customer go to the same partition, maintaining their order, while orders from different customers can be processed in parallel across different partitions.
Order = Event
Customer ID = Key
Key β Partition
Different partitions β Parallel processing
When a Kafka consumer throws an exception, the configured error handler handles it.
In Spring Kafka, we can retry the message a configured number of times, and if processing still fails, send the message to a Dead Letter Topic. Offset management depends on the configured acknowledgment and error-handling strategy.
Exception β Retry β Still fails β DLT (Dead Letter Topic)
In my microservices project, I used Kafka for asynchronous communication between services.
For example, when the Payment Service successfully completes a payment, it publishes a PaymentCompleted event to a Kafka topic. The Booking, Notification, Audit, and Reporting Services consume this event independently. We use consumer groups for parallel processing and offsets to track message consumption.
For reliability, the consumer commits the offset only after successful processing, and we use idempotent handling to avoid duplicate processing.
Reliable consumption is ensured by having the consumer process the message and only commit its offset after successful processing (manual offset commit, not auto-commit), so if the consumer crashes mid-processing, the message is redelivered rather than lost;
Idempotent processing handles the resulting possible duplicates.
Kafka stores the message β Consumer processes it β Success β Commit offset.
Consumer crashes β Offset not committed β Kafka redelivers message
Message is processed again β Duplicate possible β Use idempotent consumer using eventId
Kafka is a distributed event-streaming platform. Kafka consists mainly of producers, topics, partitions, brokers, consumers, and consumer groups.
Producers publish events to topics.
Topics are divided into partitions for scalability and parallel processing.
Partitions are stored on Kafka brokers.
Consumers read events from topics.
Consumer groups allow multiple consumer instances to process partitions in parallel.
Kafka uses offsets (Message's position inside a partition) to track message positions and replication to provide fault tolerance.
I have hands-on experience with Kafka for asynchronous communication between microservices.
I have worked with producers, consumers, topics, partitions, consumer groups, and offsets.
For example, in a Candidate Registration Portal, when an application is submitted, the Application Service publishes an ApplicationSubmitted event to Kafka, and services like Notification and Audit consume that event asynchronously.
I have also worked with concepts like retries, idempotency, and handling consumer failures.
For sub-second freshness, I would build a Kafka-based streaming pipeline where services publish events to Kafka, Kafka Streams or Flink processes them continuously, and the results are written to a time-series database such as TimescaleDB or InfluxDB. Grafana reads the latest metrics for visualization.
I would use Kafka partitions for scalability, monitor consumer lag, keep processing windows small, and use idempotent processing for reliability.
A Pod is the smallest deployable unit in Kubernetes.
It wraps one or more tightly coupled containers (usually just one) that share the same network namespace (IP address and port space) and storage volumes, and are always scheduled together on the same node.
Containers within a Pod can communicate with each other via localhost, and the Pod as a whole is what gets deployed, scaled, and replicated.
I check the Podβs exit code and events using kubectl describe pod to identify issues like OOMKilled or failed liveness/readiness probes.
I also check the Podβs resource requests/limits and probe configuration, as these can cause restarts without clear application errors in the logs.
During Kubernetes Pod termination, the Pod receives SIGTERM and is removed from receiving new traffic, while existing in-flight requests are given time to complete. In Spring Boot, I enable graceful shutdown and configure a shutdown timeout. Kubernetes also provides a termination grace period. If the application doesn't stop within that period, Kubernetes sends SIGKILL.
SIGTERM β Stop new requests β Finish in-flight requests β Grace period β Stop
I containerize each microservice as a separate Docker image using its own Dockerfile. I build the Spring Boot application as a JAR, create a lightweight image, and push it to a container registry. For local development, I use Docker Compose, and in production, I deploy the images on Kubernetes, where each service can be deployed and scaled independently.
In Kubernetes, Pods can communicate directly using Pod IPs, but since Pod IPs are temporary, we normally use a Kubernetes Service. The calling Pod uses the Service's stable DNS name, and Kubernetes routes the request to one of the destination Pods.
Kubernetes Cluster
β
βββ 1. Control Plane β Manages the cluster
β βββ API Server β Entry point
β βββ Scheduler β Chooses worker node
β βββ Controller Manager β Maintains desired state
β βββ etcd β Stores cluster data
β
βββ 2. Worker Nodes β Run applications
βββ kubelet β Node agent
βββ Container Runtime β Runs containers
βββ kube-proxy β Networking
βββ Pods β Run applications
Terraform is an Infrastructure-as-Code tool that lets you define cloud resources (servers, networks, databases) declaratively in configuration files (HCL).
It plans changes by comparing desired state to current state, then applies only the necessary changes, and tracks resource state to manage infrastructure lifecycle consistently.
Common AWS services used in backend/microservices architectures include EC2 (compute), S3 (object storage), RDS (managed relational DB), ECR/ECS/EKS (container registry/orchestration), Lambda (serverless), SQS/SNS (messaging), and CloudWatch (monitoring/logging).
ECR (Elastic Container Registry) is AWS's managed Docker container registry - it stores, manages, and lets you pull/push container images, integrating directly with ECS/EKS for deploying containerized applications on AWS.
Connect EC2 to RDS by ensuring both are in the same VPC (or peered VPCs), configuring the RDS security group to allow inbound traffic from the EC2 instance's security group on the database port, then connecting from the application using the RDS endpoint, port, and credentials via a standard JDBC connection string.
Connect a Spring Boot application to AWS by configuring the AWS SDK (or Spring Cloud AWS) with credentials (IAM roles preferred over hardcoded keys), setting the appropriate region, and using service-specific clients (e.g., S3Client, RDS via JDBC URL, SQS/SNS clients) configured through application.properties or environment variables.
Jenkins and Docker complement each other in CI/CD.
Jenkins automates the build, test, Docker image creation, image publishing, and deployment process, while Docker packages the application and its dependencies into a consistent container.
For example, when a developer pushes a Spring Boot application to Git, Jenkins checks out the code, runs Maven build and tests, creates a Docker image, pushes it to Docker Hub or Amazon ECR, and deploys that image to the target environment.
To ensure an application can handle high traffic, we use horizontal scaling, load balancing, auto scaling, caching, rate limiting, asynchronous processing, and database optimization.
For resilience, we use timeouts, retries, circuit breakers, bulkhead isolation, and graceful degradation.
This allows the system to handle traffic spikes, scale according to demand, and prevent failures from cascading across the application.
To handle millions of requests per minute, I would horizontally scale the microservice by running multiple instances behind a load balancer/API Gateway.
I would keep the services stateless, use caching for read-heavy data, database read replicas and proper indexing, asynchronous processing using Kafka/RabbitMQ for non-critical operations, and use rate limiting and circuit breakers for protection.
For consistency, I would use database transactions for local operations, optimistic/pessimistic locking where required, idempotency for operations like payment, and Saga for distributed transactions across services.
First, I would check where the bottleneck is using metrics and monitoringβCPU, memory, thread pools, database connection pool, database performance, and downstream services. I would also check API response time, error rate, and load-test results.
Then I would identify whether the bottleneck is in the application, database, network, or infrastructure and scale or optimize accordingly.
10K users β Find bottleneck β App / DB / Network / Infrastructure β Optimize or Scale
If CPU is normal but latency is high, I check thread pool exhaustion, slow database or downstream API calls, connection pool issues, lock contention, and GC pauses.
These can make requests wait without significantly increasing CPU usage.
Reproduce it by load testing in a staging environment that mirrors production scale (JMeter/Gatling), and check for race conditions, connection pool exhaustion, or resource limits that only manifest under concurrent load - issues like this often stem from shared mutable state or insufficient pool/thread sizing.
I first check centralized logs and metrics to identify which service failed first. Then I use the correlation ID or trace ID to trace the request across interconnected services. I check service health, API responses, network connectivity, database, Kafka, and recent deployments/configuration changes. I start from the first failure and trace its impact on downstream services to identify the root cause.
Multiple services failing
β
Centralized Logs / Metrics
β
Correlation ID / Trace ID
β
Find first failing service
β
Check DB / API / Kafka / Network
β
Check recent deployments/configuration changes
β
Root Cause
If the UI submits a job that needs to process billions of records, I would not process it synchronously in the API request.
I would use an asynchronous job-processing architecture. The API would accept the request, create a job ID, and put the job into a queue. Background workers would process the records in batches, and the UI could check the job status using the job ID.
Don't make the UI wait. Create a job β Queue it β Process in batches with workers β Track status
S β SRP β Single Responsibility Principle β one class, one responsibility, one reason to change
O β OCP β Open/Closed Principle β Open for Extension, Closed for Modification
L β LSP β Liskov Substitution Principle β Child must replace Parent without breaking behavior
I β ISP β Interface Segregation Principle β Many small interfaces > one large interface
D β DIP β Dependency Inversion Principle β Depend on Abstraction, not Concrete class
A Proxy acts as a middleman between the client and the actual service.
In microservices, an API Gateway commonly acts as a proxy and can handle authentication, authorization, routing, rate limiting, load balancing, and other cross-cutting concerns.
Client β Proxy/API Gateway β Microservice
Use Java 8 Streams' Collectors.groupingBy -
Map<String, Double> salData = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment, Collectors.summingDouble(Employee::getSalary)))
- to group the map's values by department and sum salaries in one pass, returning a Map<String, Double> of department to total salary.
In my project, the request starts from the frontend and reaches the API Gateway, where authentication and security checks are applied.
The gateway routes the request to the required microservice using service discovery.
Inside the service, the request goes through the controller, service layer, and repository to access its own database.
For communication between services, we use synchronous REST or asynchronous Kafka events depending on the requirement.
For resilience, we use Circuit Breaker, Retry, Timeout, and Bulkhead.
For distributed transactions, we use Saga, and each service follows Database per Service.
Observer Pattern is a design pattern where one object notifies other interested objects when its state changes.
In a microservices architecture, a similar concept is used in event-driven systems, where a service publishes an event and other interested services consume that event.
Observer = Direct notification
Event-Driven = Publish event β Consumers independently consume
For example, when a payment is completed, the Payment Service publishes a PaymentCompleted event. The Booking, Notification, Audit, and Reporting Services can consume this event independently.
Adapter Pattern is used when two systems have different interfaces.
The adapter acts as a bridge and converts one interface into another so they can communicate.
For example, in a Candidate Registration Portal, if our Payment Service and external payment gateway have different APIs, we use an Adapter to convert our request into the format expected by the payment gateway.
To develop a microservice, I first identify a clear business capability and define the service boundary.
Then I design its APIs and establish data ownership, usually following the Database per Service principle.
I implement the service using Spring Boot with controller, service, repository, DTO, validation, exception handling, and security layers.
I then add REST/gRPC or asynchronous Kafka communication as required, along with resilience patterns such as timeout, retry, and circuit breaker.
Finally, I add testing, logging, monitoring, Docker containerization, CI/CD through Jenkins, and deploy the service independently.
in my microservices architecture, I use patterns based on specific requirements.
For client communication, I use the API Gateway pattern.
For service-to-service communication, I use the Service Discovery
For data ownership, I use Database per Service.
For distributed transactions, I use the Saga pattern.
For resilience, I use Circuit Breaker, Retry, Timeout, Fallback, and Bulkhead patterns.
For reliable event publishing, I use the Transactional Outbox pattern, and for high read/write workload separation, I use CQRS.
Within individual services, I commonly use layered architecture, Repository, DTO, and Factory patterns where appropriate.