[{"content":"Kafka is a distributed, durable, append-only log — not a traditional \u0026ldquo;consume and delete\u0026rdquo; message queue. A topic is split into partitions for parallelism, each partition is replicated across brokers (one leader, N followers) for fault tolerance, and consumer groups parallelize reading (each partition read by exactly one consumer within a group). Delivery is at-least-once by default, so consumers must be idempotent — exactly-once is available via transactions. Around this core, an ecosystem (Connect, Streams/ksqlDB, Schema Registry) turns Kafka from \u0026ldquo;just a log\u0026rdquo; into a full data-integration and stream-processing platform.\nThis is written as a step-by-step glossary — each numbered section builds on the terms introduced before it.\n1. What Kafka actually is # Originally built at LinkedIn (~2010) to handle their firehose of activity/event data, then open-sourced as an Apache project. The core idea: it\u0026rsquo;s not really a message queue in the RabbitMQ/SQS sense (publish → consume → delete) — it\u0026rsquo;s a distributed, durable, append-only log that you subscribe to. Nothing gets deleted when a consumer reads it; a consumer just tracks an offset (a cursor position) and moves it forward.\nThat one design choice is why Kafka gets used for three overlapping purposes:\nPub-sub messaging — decoupling services (producer publishes, N independent consumers read, none know about each other). A durable system of record — since nothing is deleted on read, a topic can be the actual source of truth, replayable from the beginning at any time. A substrate for stream processing — tools like Kafka Streams / ksqlDB process data directly as it flows through. It\u0026rsquo;s also fast largely because it\u0026rsquo;s simple: appending to the end of a file and reading sequentially forward is about the cheapest I/O pattern that exists (sequential disk/page-cache access), versus a traditional queue\u0026rsquo;s per-message delete/ack bookkeeping.\n2. Cluster, brokers, and the controller # A Kafka cluster is a set of servers called brokers. No single broker holds all the data — partitions (see step 4) are spread across brokers so both storage and throughput scale horizontally.\nSomeone still has to track cluster-wide metadata: which topics/partitions exist, which broker is the leader for each partition, and which brokers are currently alive. Since KRaft (Kafka Raft — Kafka 3.x+, and the only mode from Kafka 4.0 onward), a subset of brokers act as controllers, forming a Raft quorum that stores this metadata in its own internal log and elects a single active controller. Older deployments used an external ZooKeeper ensemble for the same job; KRaft folds that responsibility into Kafka itself, removing the extra system to run and keep in sync.\nflowchart TB subgraph QUORUM[\"KRaft Controller Quorum\"] C1[\"Controller (active)\"] C2[\"Controller (standby)\"] C3[\"Controller (standby)\"] end subgraph CLUSTER[\"Kafka Cluster\"] B1[\"Broker 1\"] B2[\"Broker 2\"] B3[\"Broker 3\"] end QUORUM -- \"metadata log:\\ntopics, partitions,\\nleaders, live brokers\" --\u003e CLUSTER C1 -.raft consensus.-\u003e C2 C1 -.raft consensus.-\u003e C3 3. Topics # A topic is a named stream of records — a category things get published to and read from (e.g. orders, payment-events).\nMulti-producer, multi-consumer — any number of producers can write, any number of independent consumers/consumer groups can read, with no knowledge of each other. This is the actual decoupling mechanism. A record is just bytes — key, value, optional headers, timestamp. Kafka enforces no schema; if you want structure (Avro/Protobuf/JSON with a defined shape), that\u0026rsquo;s a separate layer (a Schema Registry — step 11), not something Kafka itself understands. A topic is a logical name, not the physical unit of storage or parallelism. That\u0026rsquo;s partitions. 4. Partitions \u0026amp; offsets # A topic is split into partitions — ordered, append-only logs, and the actual unit Kafka parallelizes storage and throughput across (not the topic itself). Kafka guarantees ordering within a partition only — never across partitions of the same topic.\nEvery record written to a partition gets a sequential, immutable offset — its position in that partition\u0026rsquo;s log. A consumer\u0026rsquo;s \u0026ldquo;position\u0026rdquo; is just the offset it has processed up to.\nHow a record picks its partition:\nKeyed record — the default partitioner hashes the key (hash(key) % numPartitions), so the same key always lands on the same partition — this is what preserves per-key ordering (e.g. all events for order-123 stay in order). No key — Kafka spreads records across partitions (sticky/round-robin) purely for load balancing, with no ordering guarantee between them. flowchart LR R1[\"record key=order-123\"] --\u003e H[\"hash(key) % partitions\"] R2[\"record key=order-456\"] --\u003e H R3[\"record key=order-123\"] --\u003e H H --\u003e P0[\"Partition 0\\noffsets: 0,1,2,3...\"] H --\u003e P1[\"Partition 1\\noffsets: 0,1,2...\"] H -.order-123 always here.-\u003e P0 5. Producers \u0026amp; delivery guarantees # Producers write records to topics. Two settings control how safely:\nacks — how many replicas must confirm a write before the producer considers it successful:\nacks=0 — fire and forget, don\u0026rsquo;t even wait for the leader. Fastest, weakest. acks=1 — wait for the leader to write it. If the leader dies before followers replicate, that message is gone even though the producer got a success response. acks=all (-1) — wait for every replica in the ISR (step 6). Strongest durability, higher latency. Idempotent producer (enable.idempotence=true, default in modern clients) — each producer gets a producer ID and tags every record with a sequence number. If a network hiccup makes the producer retry a send, the broker recognizes the duplicate sequence number and drops it instead of writing the record twice. This is what makes retries safe without the application having to dedupe manually.\nsequenceDiagram participant P as Producer participant L as Leader replica participant F1 as Follower 1 participant F2 as Follower 2 P-\u003e\u003eL: send record (acks=all) L-\u003e\u003eF1: replicate L-\u003e\u003eF2: replicate F1--\u003e\u003eL: ack F2--\u003e\u003eL: ack L--\u003e\u003eP: ack (all ISR confirmed) Common question: can messages arrive out of order? A producer isn\u0026rsquo;t bound to a single partition — it decides per-record where to route via an explicit partition number, key hashing (default), or round-robin when there\u0026rsquo;s no key (step 4). This has two consequences worth being explicit about:\nAcross partitions, there is no ordering guarantee at all. If message 1 goes to partition 1 and message 2 goes to partition 2, they\u0026rsquo;re independent logs with independent leaders — it\u0026rsquo;s entirely possible for message 2 to be written and read before message 1. This isn\u0026rsquo;t a bug or a race condition to fix; Kafka simply never promised ordering across partitions. Within one partition, ordering is only automatic if you use the same key for related records (so they always hash to the same partition) and enable.idempotence=true. Without idempotence, a producer with multiple requests in flight (max.in.flight.requests.per.connection \u0026gt; 1) can have a retried record land after a later one that succeeded on the first try — reordering the same partition\u0026rsquo;s log. Idempotence prevents this because the broker tracks each producer\u0026rsquo;s sequence numbers and can detect/reject out-of-sequence writes. So: same key → same partition → in-order, as long as idempotence is on. Different keys or no key → no ordering promise between those records, ever.\n6. Replication, leader election, and ISR # Each partition has a replication factor (commonly 3): one broker holds the leader replica, the others hold follower replicas.\nFollowers don\u0026rsquo;t get pushed data — they pull from the leader, using the same fetch mechanism a normal consumer uses. No separate replication protocol. The set of replicas caught up enough to be trustworthy is the ISR — in-sync replicas. A follower that falls too far behind (configurable lag threshold) gets dropped from the ISR until it catches up. If the leader dies, a new leader is elected only from the ISR — never from a lagging replica, since that could silently lose committed data. Gotcha: acks=all only guarantees what \u0026ldquo;all\u0026rdquo; currently means. If the ISR has shrunk to just the leader (followers all lagged out), \u0026ldquo;all\u0026rdquo; means \u0026ldquo;one.\u0026rdquo; min.insync.replicas guards against this — it sets a floor (e.g. 2) below which the leader refuses writes rather than silently downgrading the durability guarantee.\nsequenceDiagram participant Ctrl as Controller participant L as Leader (Broker 1) participant F as Follower (Broker 2, in ISR) Note over L: Broker 1 crashes Ctrl-\u003e\u003eCtrl: detect missed heartbeat Ctrl-\u003e\u003eF: elect as new leader (was in ISR) Ctrl--\u003e\u003eCtrl: update metadata (new leader = Broker 2) Note over F: Broker 2 now serves\\nproduce/consume for this partition 7. Consumers \u0026amp; consumer groups # Consumers read records; they\u0026rsquo;re organized into consumer groups. Within a given group, each partition is read by exactly one consumer — that\u0026rsquo;s how Kafka parallelizes consumption (add more consumers, up to one per partition, for more parallelism).\nEach group tracks its own progress independently, via committed offsets stored in an internal topic (__consumer_offsets). This means multiple, unrelated consumer groups can read the same topic at completely different paces without affecting each other — e.g. a real-time alerting group and a nightly batch-analytics group reading the same orders topic.\nDefault delivery guarantee is at-least-once (so consumers must be idempotent — the same event can arrive twice); stronger guarantees are covered in step 10.\n8. Rebalancing # Rebalancing happens whenever consumer group membership changes — a consumer joins, leaves, or crashes (missed heartbeat) — and partitions need to be redistributed among the remaining/new consumers.\nsequenceDiagram participant C1 as Consumer 1 participant C2 as Consumer 2 (new) participant Coord as Group Coordinator (broker) C2-\u003e\u003eCoord: join group Coord-\u003e\u003eC1: revoke partitions Coord-\u003e\u003eCoord: run partition assignor Coord-\u003e\u003eC1: assign new partition subset Coord-\u003e\u003eC2: assign new partition subset Note over C1,C2: consumption resumes Older (\u0026ldquo;eager\u0026rdquo;) rebalancing revokes all partitions from every consumer before reassigning — a brief stop-the-world pause for the whole group. Incremental cooperative rebalancing only moves the partitions that actually need to move, letting unaffected consumers keep processing throughout.\n9. Retention \u0026amp; log compaction # Retention is configured per topic, and controls when old data disappears:\nTime-based — e.g. keep records for 7 days, then delete. Size-based — cap the partition\u0026rsquo;s total size. Compaction — instead of deleting by age, keep only the latest value per key, forever. Used when a topic represents \u0026ldquo;current state\u0026rdquo; rather than \u0026ldquo;history of events\u0026rdquo; (e.g. a changelog of user profiles, or Kafka Streams\u0026rsquo; internal changelog topics from step 13). Writing a record with a null value for a key (a tombstone) marks that key for deletion once compaction runs. flowchart LR subgraph BEFORE[\"Before compaction\"] direction TB A1[\"K1=v1\"] --\u003e A2[\"K2=v2\"] --\u003e A3[\"K1=v3\"] --\u003e A4[\"K3=v4\"] --\u003e A5[\"K1=v5\"] end subgraph AFTER[\"After compaction\"] direction TB B2[\"K2=v2\"] --\u003e B4[\"K3=v4\"] --\u003e B5[\"K1=v5\"] end BEFORE -. \"compaction: keep\\nonly latest per key\" .-\u003e AFTER 10. Delivery semantics \u0026amp; exactly-once # Three levels, in order of how much work they take to get:\nAt-most-once — no retries; a message can be lost, never duplicated. At-least-once — Kafka\u0026rsquo;s default; retries can create duplicates, so consumers must be idempotent. Exactly-once — built from two pieces: the idempotent producer (step 5, dedupes retries on write) plus transactions. A transactional producer (given a transactional.id) can write to multiple partitions/topics and mark them committed atomically; a consumer with isolation.level=read_committed only ever sees records from committed transactions, skipping aborted ones entirely. This is exactly how Kafka Streams gets exactly-once for a consume → transform → produce pipeline. sequenceDiagram participant Src as Input topic participant App as Stream processor (transactional) participant Dst as Output topic App-\u003e\u003eSrc: consume batch (isolation.level=read_committed) App-\u003e\u003eApp: transform App-\u003e\u003eApp: beginTransaction() App-\u003e\u003eDst: produce results App-\u003e\u003eSrc: commit consumer offsets (as part of txn) App-\u003e\u003eApp: commitTransaction() Note over Dst: downstream read_committed\\nconsumers only see this\\nafter commit succeeds 11. Schema Registry # Kafka itself doesn\u0026rsquo;t understand or enforce record structure — a value is just bytes. A Schema Registry (Confluent\u0026rsquo;s, or open-source alternatives) adds that structure back as a separate service: it stores schema definitions (commonly Avro, Protobuf, or JSON Schema), and producer/consumer serializers talk to it.\nA producer\u0026rsquo;s serializer registers (or looks up) the schema, then writes only a small schema ID alongside the encoded bytes — not the whole schema on every record. A consumer\u0026rsquo;s deserializer fetches the schema by that ID to decode the bytes correctly. Compatibility modes (backward / forward / full) let the registry reject a schema change that would break existing producers or consumers, giving you safe schema evolution over time. flowchart LR P[\"Producer\"] --\u003e|\"register/lookup schema\"| SR[\"Schema Registry\"] P --\u003e|\"record: [schema ID][avro bytes]\"| T[\"Kafka topic\"] T --\u003e C[\"Consumer\"] C --\u003e|\"fetch schema by ID\"| SR 12. Kafka Connect # Kafka Connect is a framework for moving data in and out of Kafka without writing custom producer/consumer code.\nSource connectors pull data from an external system (a database, files, an API) into a Kafka topic. Sink connectors push data from a Kafka topic into an external system (Elasticsearch, S3, a data warehouse). Connectors run as distributed workers, and each connector splits its work into parallel tasks. flowchart LR DB[(\"Source DB\")] --\u003e SRC[\"Source Connector\"] SRC --\u003e T[\"Kafka topic\"] T --\u003e SINK[\"Sink Connector\"] SINK --\u003e DW[(\"Data Warehouse\")] 13. Kafka Streams / ksqlDB # Kafka Streams is a client library (not a separate cluster) for processing data directly as it flows through Kafka — reading from input topics, transforming, and writing to output topics. ksqlDB puts a SQL layer on top of the same engine, so you can express stream processing as SQL-like queries instead of Java/Scala code.\nStateful operations (aggregations, joins, windowed counts) keep their working state in a local state store, which is continuously backed up to an internal changelog topic (a compacted topic — step 9) so state survives a crash or gets rebuilt on another instance.\nflowchart LR IN[\"Input topic:\\nraw-clicks\"] --\u003e PROC[\"Stream processor\\n(filter, group, window)\"] PROC \u003c--\u003e|\"backs up to\"| CL[\"Changelog topic\\n(compacted)\"] PROC --\u003e STORE[(\"Local state store\")] PROC --\u003e OUT[\"Output topic:\\nclicks-per-minute\"] 14. Security basics # Three independent layers, commonly used together:\nEncryption in transit — TLS between clients and brokers. Authentication — proving who a client is: SASL mechanisms (PLAIN, SCRAM) or mutual TLS (mTLS, using client certificates). Authorization — ACLs decide what an authenticated principal is allowed to do (produce to topic X, consume from topic Y, create topics, etc.). flowchart LR CL[\"Client\"] --\u003e|\"1. TLS handshake\"| B[\"Broker\"] CL --\u003e|\"2. SASL/mTLS auth\"| B B --\u003e|\"3. check ACL for principal + topic + operation\"| DEC{\"Allowed?\"} DEC --\u003e|yes| OK[\"Request served\"] DEC --\u003e|no| DENY[\"AuthorizationException\"] 15. Quotas \u0026amp; monitoring # Quotas cap the byte-rate (or request rate) a given client/user can push or pull, so one noisy producer or consumer can\u0026rsquo;t starve everyone else sharing the cluster.\nThe single most important thing to watch operationally is consumer lag — the gap between a partition\u0026rsquo;s latest offset and a consumer group\u0026rsquo;s committed offset. Growing lag means a consumer group is falling behind the rate records are being produced. It\u0026rsquo;s typically tracked via Kafka\u0026rsquo;s JMX metrics, exported to Prometheus, and graphed in Grafana (or tools like Burrow built specifically for lag tracking).\n16. Architecture at a glance # A worked example tying the terms above together: topic orders, 3 partitions, replication factor 3, one consumer group (analytics-group) with 2 consumers.\nflowchart LR subgraph PR[\"Producers\"] P1[\"order-service\"] P2[\"payment-service\"] end CTRL[\"Controller\\n(KRaft quorum)\\ntracks metadata,\\nelects leaders\"] subgraph CLUSTER[\"Kafka Cluster — topic: orders\"] direction LR subgraph B1[\"Broker 1\"] B1P0[\"P0 — LEADER\"] B1P1[\"P1 — replica\"] B1P2[\"P2 — replica\"] end subgraph B2[\"Broker 2\"] B2P1[\"P1 — LEADER\"] B2P0[\"P0 — replica\"] B2P2[\"P2 — replica\"] end subgraph B3[\"Broker 3\"] B3P2[\"P2 — LEADER\"] B3P0[\"P0 — replica\"] B3P1[\"P1 — replica\"] end end subgraph CG[\"Consumer Group: analytics-group\"] C1[\"consumer-1\\nreads P0\"] C2[\"consumer-2\\nreads P1, P2\"] end P1 --\u003e CLUSTER P2 --\u003e CLUSTER CTRL -.manages.-\u003e B1 CTRL -.manages.-\u003e B2 CTRL -.manages.-\u003e B3 B1P0 --\u003e C1 B2P1 --\u003e C2 B3P2 --\u003e C2 Note how leaders are spread round-robin across brokers (Broker 1 leads P0, Broker 2 leads P1, Broker 3 leads P2) rather than piling onto one broker — that\u0026rsquo;s real Kafka behavior, not a simplification, and it\u0026rsquo;s what keeps write load balanced across the cluster.\nUse cases # Messaging backbone (decoupled pub-sub) # Kafka replaces point-to-point integrations between services with one shared log. order-service publishes to a topic without knowing who reads it; email-service, fraud-detection, and analytics each read independently, at their own pace, and a new consumer can be added later without touching the producer at all.\nCentralized log aggregation # Many services each emit logs; instead of each one shipping directly to a log store, they all publish to Kafka, and one pipeline reads from Kafka into the actual store (Elasticsearch, S3, a data lake). Kafka absorbs bursts and buffers the store from load spikes it can\u0026rsquo;t otherwise handle in real time.\nEvent sourcing # Instead of storing only current state, the topic itself is the source of truth — every state change is appended as an event, forever (or compacted to latest-per-key, step 9). Application state is a materialized view that\u0026rsquo;s rebuilt by replaying the log from the beginning, which also gives you a full audit trail for free.\nChange Data Capture (CDC) # A CDC connector (e.g. Debezium, run via Kafka Connect — step 12) tails a database\u0026rsquo;s write-ahead log and publishes every row-level change to a Kafka topic in near real time — without the application code ever having to publish anything itself. Downstream, a search index, a cache, and a data warehouse can each independently stay in sync with the source database.\nReal-time stream processing # Kafka Streams or ksqlDB (step 13) continuously transform, filter, join, and aggregate data as it arrives — e.g. turning a raw clicks topic into a clicks-per-minute topic — and write the result back to Kafka or out to a live dashboard, with no batch job or nightly cron involved.\n","date":"17 September 2026","externalUrl":null,"permalink":"/blogs/kafka/apache-kafka-concepts/","section":"Blogs","summary":"","title":"Apache Kafka Concepts","type":"blogs"},{"content":"Claude\u0026rsquo;s API exposes tool use/function calling — the model decides when to call a tool you\u0026rsquo;ve defined, your code executes it, and the result feeds back into the conversation. That\u0026rsquo;s the real machinery behind \u0026ldquo;agentic\u0026rdquo; workflows, best framed as a plan → act → observe/reflect loop with human-in-the-loop guardrails rather than fully autonomous execution.\nThe Claude API is Anthropic\u0026rsquo;s REST API (POST /v1/messages): a messages array plus an optional system prompt go in, a response comes out. On top of that, tool use / function calling lets you define tools via JSON schema; the model decides when to call one, your code executes it, and the result is fed back into the conversation for the model to continue reasoning with. This single mechanism is the underlying machinery for essentially all \u0026ldquo;agentic\u0026rdquo; behavior — everything else is built on top of that same call-tool, get-result, keep-reasoning loop.\nGitHub Copilot sits at a different layer: inline, context-aware code completion plus chat-based assistance directly in the IDE, rather than an autonomous tool-calling loop.\nThe useful mental model for an \u0026ldquo;agentic workflow\u0026rdquo; is a plan → act → observe/reflect loop: the model plans a step, takes an action (a tool call), observes the result, and decides the next step. The important qualifier is that this should ideally run with human-in-the-loop guardrails — review gates, static analysis, test suites — rather than letting it run fully autonomously end to end.\n","date":"14 September 2026","externalUrl":null,"permalink":"/blogs/ai/agentic-workflows/","section":"Blogs","summary":"","title":"AI-Assisted Development and Agentic Workflows","type":"blogs"},{"content":"Domain-Driven Design rests on three load-bearing concepts: the bounded context, the ubiquitous language, and the aggregate.\nA bounded context is a boundary within which a model is valid and internally consistent. The same term can mean something different depending on which context it\u0026rsquo;s used in — \u0026ldquo;Customer\u0026rdquo; in a Billing context might carry billing address and payment methods, while \u0026ldquo;Customer\u0026rdquo; in a Support context carries ticket history and support tier. Rather than forcing one shared \u0026ldquo;Customer\u0026rdquo; model across the whole system, DDD accepts that each context gets its own model of the term, valid only within its own boundary. Bounded contexts are also a common lens for drawing microservice boundaries — see Microservices Architecture Fundamentals.\nThe ubiquitous language is a vocabulary shared between engineers and domain experts, and — critically — reflected directly in code: class names, method names, module names. The point is to stop a term from being precise in conversation with a domain expert but then getting lost or renamed once it hits the codebase; the same word should mean the same thing whether you\u0026rsquo;re in a requirements meeting or reading a class definition.\nAn aggregate is a cluster of related objects treated as a single consistency boundary. Changes to anything inside the aggregate go through one entry point — the aggregate root — which is the only object allowed to enforce the aggregate\u0026rsquo;s invariants. Nothing outside the aggregate is allowed to reach in and modify an internal object directly; it has to go through the root, which is what keeps the aggregate\u0026rsquo;s invariants actually enforceable.\n","date":"14 September 2026","externalUrl":null,"permalink":"/blogs/architecture/domain-driven-design-basics/","section":"Blogs","summary":"","title":"Domain-Driven Design Basics","type":"blogs"},{"content":"Event-driven systems are built around pub-sub: producers publish events without knowing who — or whether anyone — consumes them. That lack of a direct dependency between producer and consumer is what decouples services from each other in the first place.\nTwo coordination styles sit on top of that same pub-sub foundation. Choreography has each service react to events independently, with no central coordinator directing the flow — good for loose coupling, but harder to trace an end-to-end flow since the logic is spread across every participant. Orchestration puts a central process in charge, explicitly sequencing calls to the other services — easier to trace and reason about as a single flow, but that orchestrator itself becomes a coupling point that every participant now depends on.\nWhichever style is used, consumers need to be idempotent. Most message brokers, Kafka included, deliver at-least-once by default, which means the same event can arrive — and get processed — more than once. If handling an event isn\u0026rsquo;t safe to repeat, a duplicate delivery becomes a bug, not just an inefficiency. See Apache Kafka Concepts for how Kafka\u0026rsquo;s delivery guarantees work under the hood.\n","date":"14 September 2026","externalUrl":null,"permalink":"/blogs/architecture/event-driven-patterns/","section":"Blogs","summary":"","title":"Event-Driven Architecture Patterns","type":"blogs"},{"content":"Microservices are services bounded by business capability, each owning its own data store, independently deployable and independently scalable. The hard parts are the boundaries between them: what talks sync vs. async, how consistency is kept without distributed transactions, and how failures in one service don\u0026rsquo;t cascade into all of them.\nCore definition # Services bounded by business capability, each owning its own data store, independently deployable and independently scalable. \u0026ldquo;Database-per-service\u0026rdquo; — no service reaches directly into another\u0026rsquo;s database — is the load-bearing rule that makes independent deployability actually hold; a shared DB across services re-couples them even if the code is split.\n","date":"14 September 2026","externalUrl":null,"permalink":"/blogs/architecture/microservices-fundamentals/","section":"Blogs","summary":"","title":"Microservices Architecture Fundamentals","type":"blogs"},{"content":"Spring Boot removes manual Spring configuration through three mechanisms: auto-configuration (conditional bean registration based on classpath + existing beans), an embedded server plus starter dependencies (curated dependency bundles), and production-ready defaults via Actuator. The bean lifecycle follows a fixed 9-step sequence, dependency injection should default to the constructor form, and a \u0026ldquo;starter\u0026rdquo; is a dependency descriptor, not runtime code.\nFor what @SpringBootApplication itself actually does under the hood, see What Really Happens When You Add @SpringBootApplication? — in short, it\u0026rsquo;s a meta-annotation combining @SpringBootConfiguration, @EnableAutoConfiguration (the mechanism below), and @ComponentScan. The default embedded server for a typical web app is Tomcat, via Spring MVC (not Jersey).\nDependency injection and the bean lifecycle # Two separate steps happen: (a) bean registration — stereotype annotations (@Component, @Service, @Repository, @Controller) mark a class to be picked up by component scanning; (b) dependency injection — the actual wiring of one bean into another, via constructor (preferred) or @Autowired field/setter.\nFull bean lifecycle, in order:\nflowchart TD A[\"1. Bean definitions loaded\\n(component scan / @Configuration)\"] --\u003e B[\"2. Instantiation\\n(constructor called)\"] B --\u003e C[\"3. Dependency injection\\n(fields/setters populated)\"] C --\u003e D[\"4. Aware callbacks\\n(BeanNameAware, ApplicationContextAware...)\"] D --\u003e E[\"5. BeanPostProcessor\\npostProcessBeforeInitialization\"] E --\u003e F[\"6. Init callbacks\\n@PostConstruct -\u003e afterPropertiesSet() -\u003e init-method\"] F --\u003e G[\"7. BeanPostProcessor\\npostProcessAfterInitialization\\n(AOP proxy created HERE)\"] G --\u003e H[\"8. Bean ready\\n(cached in container)\"] H --\u003e I[\"9. Destruction (singleton only)\\n@PreDestroy -\u003e destroy() -\u003e destroy-method\"] Say it out loud as: load → build → wire → make aware → pre-init hook → initialize → post-init hook (proxy wraps here) → ready → destroy.\nScopes: singleton (default — one instance per container), prototype (a new instance every injection/lookup), and the web-aware scopes — request, session, application, websocket.\nUse prototype for a bean holding mutable, non-thread-safe state per use (e.g. a stateful builder/accumulator) — you don\u0026rsquo;t want threads sharing one instance. Use request scope for web-tier state that should live only for one HTTP request (e.g. a resolved tenant/user context cached so you don\u0026rsquo;t re-resolve it in every layer). Gotcha worth knowing: a singleton bean injecting a prototype bean gets only one prototype instance forever (wired once at startup) unless you use a scoped proxy (proxyMode = ScopedProxyMode.TARGET_CLASS) or ObjectProvider/ObjectFactory to fetch a fresh instance on demand. Also, Spring does not call destroy callbacks on prototype beans — cleanup of those is the caller\u0026rsquo;s responsibility.\nHow auto-configuration decides what to configure # @EnableAutoConfiguration reads META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (a plain text list of hundreds of candidate @Configuration classes bundled in spring-boot-autoconfigure.jar — e.g. DataSourceAutoConfiguration, TomcatServletWebServerFactoryAutoConfiguration). Every one of these is evaluated on every startup, gated by conditional annotations:\n@ConditionalOnClass — applies only if a given class is on the classpath (why adding a JDBC driver jar \u0026ldquo;turns on\u0026rdquo; DataSourceAutoConfiguration — it\u0026rsquo;s classpath detection, not the starter doing anything special). @ConditionalOnMissingBean — applies only if you haven\u0026rsquo;t already defined your own bean of that type (your @Bean always wins over the auto-configured default). @ConditionalOnProperty, @ConditionalOnWebApplication — gate on config properties / app type. Ordering between auto-config classes is controlled via @AutoConfigureAfter/@AutoConfigureBefore.\nflowchart TD A[\"App starts — @EnableAutoConfiguration fires\"] --\u003e B[\"Read AutoConfiguration.imports\\n(hundreds of candidate @Configuration classes)\"] B --\u003e C{\"For each candidate:\\nconditions satisfied?\"} C --\u003e|\"@ConditionalOnClass matches\\nAND no conflicting bean\"| D[\"Bean registered\\n(positive match)\"] C --\u003e|\"condition fails\"| E[\"Skipped\\n(negative match)\"] D --\u003e F[\"Visible via --debug -\u003e\\nConditions Evaluation Report\"] E --\u003e F Debugging \u0026ldquo;why isn\u0026rsquo;t X getting auto-configured\u0026rdquo;: run with --debug (or debug=true in properties) — prints the Conditions Evaluation Report on startup: every auto-configuration class split into \u0026ldquo;Positive matches\u0026rdquo; and \u0026ldquo;Negative matches,\u0026rdquo; each with the exact reason (e.g. \u0026ldquo;did not match: required class \u0026lsquo;javax.sql.DataSource\u0026rsquo; was not found\u0026rdquo;). Also available at runtime via /actuator/conditions. Force-disable one explicitly with @SpringBootApplication(exclude = DataSourceAutoConfiguration.class). One-liner: \u0026ldquo;classpath + conditional annotations + a text file listing candidate configs.\u0026rdquo;\nDiagnosing a REST endpoint that\u0026rsquo;s fast in isolation but slow under load # The core insight: a single request never contends for a shared, finite resource — many concurrent requests do. Ranked causes, roughly most-to-least common:\nThread pool exhaustion — Tomcat\u0026rsquo;s embedded worker pool is fixed (default ~200). If a handler does anything blocking (JDBC call, blocking HTTP call), each concurrent request holds a thread for the duration; once concurrent requests exceed the pool size, new requests queue and latency falls off a cliff. (This is exactly the problem a reactive/WebFlux rewrite solves — see Spring WebFlux and Reactive Programming.) Connection pool exhaustion — HikariCP\u0026rsquo;s default pool is ~10 connections; more concurrent requests than pooled connections means requests queue for one. GC pressure — higher allocation rate under load means more frequent/longer GC pauses, adding to every request\u0026rsquo;s latency (a throughput effect, distinct from a true memory leak, which shows as heap climbing without recovering over time). CPU saturation — concurrent requests compete for the same cores. Downstream amplification — a dependency with spare capacity for one request starts queueing once several requests hit it concurrently. Diagnostic order — the mental checklist to run through, cheapest/most-likely check first:\nflowchart TD A[\"Endpoint slow only under load\"] --\u003e B{\"tomcat.threads.busy\\nnear config.max?\"} B --\u003e|yes| B1[\"Thread pool exhaustion:\\nfind the blocking call in the handler\"] B --\u003e|no| C{\"hikaricp.connections.pending \u003e 0?\"} C --\u003e|yes| C1[\"Connection pool exhaustion:\\nsize the pool / cut hold time\"] C --\u003e|no| D{\"jvm.gc.pause rising, or\\nheap climbing without recovery?\"} D --\u003e|yes| D1[\"GC pressure or real leak:\\ncheck heap trend over time\"] D --\u003e|no| E{\"CPU near saturation?\"} E --\u003e|yes| E1[\"CPU-bound:\\nscale out or optimize hot path\"] E --\u003e|no| F[\"Downstream dependency amplification:\\ncheck its latency under concurrency\"] Backing metrics for each check: tomcat.threads.busy vs .config.max → hikaricp.connections.pending/.active vs .max → jvm.gc.pause + jvm.memory.used vs .max → http.server.requests p50/p95/p99 (reveals \u0026ldquo;fine alone, bad under load\u0026rdquo; since p50 can look fine while p99 explodes) → /actuator/threaddump for a live incident (many threads BLOCKED/WAITING on the same lock/resource is the smoking gun). In practice these are scraped into Prometheus/Grafana rather than read raw.\nBuilding a custom Spring Boot starter # A starter is a dependency descriptor, not runtime code — a POM/Gradle module bundling a curated, version-compatible set of dependencies (that\u0026rsquo;s literally what spring-boot-starter-web is: no code, just pulls in spring-webmvc + Jackson + embedded Tomcat, etc.).\nStandard two-module convention:\nacme-spring-boot-autoconfigure — the real code: @Configuration classes gated by @ConditionalOnClass/@ConditionalOnMissingBean, @ConfigurationProperties classes so consumers can override defaults via application.yml, and the AutoConfiguration.imports file registering those configs (same mechanism as above). acme-spring-boot-starter — an empty POM depending on the autoconfigure module plus whatever third-party client library it wraps. This is the artifact app teams actually add. Why over a shared library module: a plain shared library still requires every consuming team to hand-write @Bean wiring and guess at sensible config — boilerplate that drifts slightly per team. A starter gives every team the same org-approved defaults out of the box — \u0026ldquo;add one dependency, get a working, pre-configured client.\u0026rdquo; Examples: a company-wide Kafka-client starter with standardized security/serialization config, a shared observability starter with tracing pre-wired, a resilience-wrapped HTTP client starter with retries/circuit-breaker baked in. It\u0026rsquo;s convention-over-configuration applied at the organization level, not just the app level.\nConstructor injection vs. field injection # Use constructor injection by default (the Spring team\u0026rsquo;s own recommendation). Reasons, in order of strength:\nTestability — plain new MyClass(mockDep) in a unit test, no Spring context, no reflection, no setters needed. Immutability — dependencies can be declared private final; the object is fully valid the instant it exists, with no half-wired state (field injection populates non-final fields after the no-arg constructor runs). Fail-fast on circular dependencies — if class A and class B depend on each other via constructors, the app refuses to start with a clear circular-dependency error. Field/setter injection can silently resolve such cycles via early bean references — meaning a real design smell keeps working instead of forcing a fix. Visible dependency graph — a constructor with eight parameters is an obvious, unmissable \u0026ldquo;this class does too much\u0026rdquo; signal; the same eight dependencies as scattered @Autowired fields are easy to not notice accumulating. One-liner: \u0026ldquo;constructor injection — immutable, fail-fast, testable without a container, makes an overloaded class visually obvious.\u0026rdquo;\n","date":"14 September 2026","externalUrl":null,"permalink":"/blogs/spring/core-concepts/","section":"Blogs","summary":"","title":"Spring Boot Core Concepts","type":"blogs"},{"content":"WebFlux is Spring\u0026rsquo;s reactive stack, built on Project Reactor — lazy, backpressure-aware streams (Mono/Flux) running on a small event-loop of threads (Netty) instead of one thread blocked per request. It\u0026rsquo;s a concurrency model, not an architecture style — reactive programming and microservices are separate concepts.\nMono = 0–1 result, Flux = 0–N results — the two core reactive types from Project Reactor. Reactive streams are lazy — nothing executes until something subscribes. Backpressure — a slow consumer can signal a fast producer to slow down, instead of being overwhelmed or dropping data. Runs on an event-loop model (Netty): a small, fixed number of threads handle many concurrent requests via non-blocking I/O, instead of one thread blocked per request waiting on I/O. This is precisely what fixes the classic \u0026ldquo;thread pool exhaustion under load\u0026rdquo; failure mode — see Spring Boot Core Concepts (diagnosing a slow-under-load REST endpoint). Blocking calls (JDBC, blocking HTTP) tie up a scarce worker thread for their duration under the traditional Spring MVC/Tomcat model; WebFlux threads never sit idle waiting on I/O. Reactive programming ≠ microservices. WebFlux is an in-process concurrency model (how one service handles concurrent requests internally); microservices is a distributed-systems architecture style (how many independent services are organized). Don\u0026rsquo;t conflate the two in an interview answer. ","date":"14 September 2026","externalUrl":null,"permalink":"/blogs/spring/webflux-reactive-programming/","section":"Blogs","summary":"","title":"Spring WebFlux and Reactive Programming","type":"blogs"},{"content":"WebSockets give a persistent, full-duplex connection so a server can push data anytime (unlike request/response HTTP). Java servers hold thousands of these open cheaply via an event-loop (small selector thread pool + worker thread pool), but bursts of blocking work saturate the fixed worker pool. Virtual threads (Java 21+) make thread-per-task cheap again at scale by unmounting from real \u0026ldquo;carrier\u0026rdquo; threads whenever they block — but pinning (inside synchronized, pre-Java 24) defeats that benefit.\nWebSockets vs HTTP # Plain HTTP is one-shot: client asks, server answers, done. That\u0026rsquo;s bad for chat/live apps because the server needs to push data unprompted. A WebSocket starts as an HTTP request that \u0026ldquo;upgrades\u0026rdquo; (Connection: Upgrade) into a persistent TCP connection both sides can write to at any time — no polling, no per-message request overhead, and the server can initiate messages (which plain HTTP can\u0026rsquo;t do).\nConcrete contrast for a chat app:\nHTTP polling: browser does GET /messages every N seconds regardless of whether anything happened; sending a message is a separate POST. Wasteful and laggy (you only see new messages on your next poll). WebSocket: one upgrade handshake, then messages flow instantly in both directions over the same open connection. How Java servers hold thousands of connections open # Two strategies:\nBlocking I/O (old-school, thread-per-connection): one OS thread per open connection, parked in socket.read() doing nothing until data arrives. Simple, but each Java thread costs ~1MB stack + real OS scheduling overhead — 10,000 open connections ≈ 10,000 idle threads ≈ ~10GB RAM. This is the classic C10K problem. Non-blocking I/O / event loop (modern default — Tomcat/Jetty/Netty): a small number of \u0026ldquo;selector\u0026rdquo; threads (matching CPU core count) use OS-level multiplexing (epoll) to watch thousands of sockets at once and get woken only when a socket actually has data. The real work (running onMessage) is then handed off to a separate worker thread pool; threads aren\u0026rsquo;t dedicated to a connection, they\u0026rsquo;re shared and only busy when there\u0026rsquo;s real work. What happens under a sudden surge (e.g. 10,000 concurrent requests) # Two separate bottlenecks:\nAccepting connections: cheap with an event-loop server — mostly just registering a socket with the selector, so this scales well even under a burst.\nProcessing the work: bounded by the worker pool size (e.g. 200 threads). If 10,000 messages arrive at once and each handler is fast, the queue drains quickly. But if handlers block (DB calls, slow network calls to other services), those pool threads stay tied up, the queue backs up hard, and:\nunbounded queue → memory balloons, risk of OOM bounded queue → once full, new work gets rejected outright or times out Net: you don\u0026rsquo;t get a graceful slowdown, you hit a wall determined by how many platform threads you can afford to keep alive (each one costs real memory).\nVirtual threads (Java 21+, Project Loom) and how they change this # Virtual threads decouple \u0026ldquo;a thread as a unit of concurrency in code\u0026rdquo; from \u0026ldquo;a thread as an OS resource.\u0026rdquo; They look and behave like normal Threads in code, but run on top of a small pool of real OS threads called carrier threads. When code on a virtual thread hits a blocking call (socket read, JDBC call, Thread.sleep), the JVM unmounts it from its carrier thread, freeing that carrier to run some other virtual thread; when the blocking op completes, the virtual thread remounts onto any free carrier and continues. A blocked virtual thread costs almost nothing (a few hundred bytes on heap, no dedicated OS stack), so you can have millions of them.\nEffect on the surge scenario: spin up one virtual thread per task/connection freely. Each blocks individually on its DB call, but that doesn\u0026rsquo;t tie up a scarce OS thread — it just parks cheaply, and the small set of carrier threads keeps getting reused by whichever virtual threads are ready to run. The old thread-per-connection model (simple, blocking, easy-to-read code — no callbacks/reactive gymnastics) becomes viable again at scale.\nFor Java WebSockets concretely: Tomcat 10.1+/Jetty 12+ can be configured to run request/message handling on a virtual-thread-per-task executor, so each onMessage call gets its own (cheap) virtual thread instead of borrowing from a small fixed pool.\nThe catch: pinning # Pinning is when a virtual thread blocks but can\u0026rsquo;t unmount — it holds its carrier thread the whole time, just like an old platform thread. Enough pinned virtual threads at once and you\u0026rsquo;re back to carrier-thread starvation.\nCauses:\nInside a synchronized block/method: blocking while holding a synchronized lock pins the carrier for the whole block. Fixed in Java 24 (JEP 491) — but a real trap on Java 21-23, especially since older WebSocket frameworks or shared-state code (e.g. a synchronized Map\u0026lt;String, Session\u0026gt; tracking connected clients) commonly use synchronized instead of java.util.concurrent.locks.ReentrantLock. Native/JNI code: the JVM can\u0026rsquo;t see into native calls, so it can\u0026rsquo;t unmount around them. Mitigations: swap synchronized for ReentrantLock in hot paths, upgrade to Java 24+ where possible, detect pinning via JFR events or -Djdk.tracePinnedThreads=full.\nTwo more honest caveats:\nVirtual threads help I/O-bound concurrency only — zero benefit for CPU-bound handler work (still limited by core count). Don\u0026rsquo;t pool virtual threads (no Executors.newFixedThreadPool equivalent) — the model is create-cheaply-per-task, not reuse; pooling them is an anti-pattern carried over from platform-thread habits. ","date":"2 September 2026","externalUrl":null,"permalink":"/blogs/java/websockets-threading-virtual-threads/","section":"Blogs","summary":"","title":"WebSockets in Java: Threading, Scaling, and Virtual Threads","type":"blogs"},{"content":"Inversion of Control (IoC) is the principle that flips who controls object creation and program flow from your code to a framework/container. Dependency Injection (DI) is just one technique that implements IoC — specifically applied to how dependencies get created and handed to your classes.\nIoC vs DI # These are often used interchangeably, but IoC is the broader principle and DI is one specific flavor of it.\nWithout IoC, your class controls its own dependency creation:\npublic class UserController { private UserService userService = new UserService(); // I create it myself } The class decides what to instantiate and when — it\u0026rsquo;s \u0026ldquo;in control.\u0026rdquo;\nWith IoC (via Spring), that control moves out of the class and into the container (ApplicationContext). The class just declares what it needs, and the container decides what to create and hands it over:\n@Controller public class UserController { private final UserService userService; @Autowired public UserController(UserService userService) { // given to me, not created by me this.userService = userService; } } DI (constructor/setter/field injection) is the mechanism Spring uses to actually deliver the dependency once control has been inverted.\nIoC is broader than just dependency creation. A classic way to describe it is the Hollywood Principle: \u0026ldquo;Don\u0026rsquo;t call us, we\u0026rsquo;ll call you.\u0026rdquo; This shows up in Spring even outside of DI — you never call your @Controller\u0026rsquo;s method yourself; the DispatcherServlet calls it for you when a request arrives. That\u0026rsquo;s IoC applied to flow of control (who invokes your code and when), as opposed to DI, which is IoC applied to object creation.\nIoC ≠ loose coupling. Loose coupling (depending on an abstraction/interface rather than a concrete class) is a common benefit enabled by DI, but it\u0026rsquo;s not the definition of IoC itself. You can still have IoC with tight coupling if you inject a concrete class instead of an interface — coupling is a separate axis from who controls creation/flow.\nSummary of the mental model:\nIoC = who\u0026rsquo;s driving (the framework decides what runs/gets created and when). DI = the specific case of IoC applied to handing your class its dependencies, instead of it constructing them itself. ","date":"25 August 2026","externalUrl":null,"permalink":"/blogs/architecture/dependency-injection-and-ioc/","section":"Blogs","summary":"","title":"Dependency Injection and Inversion of Control","type":"blogs"},{"content":"Code goes from a local Dockerfile → built image → pushed to a registry → deployed via CI/CD or GitOps using hand-written Kubernetes manifests (never auto-converted from docker-compose.yml) → scheduled onto a node → actually started by the kubelet/container runtime → served to real traffic via a Service/Ingress.\nflowchart TD subgraph P1[\"Phase 1 — your machine\"] A1[\"01 Write Dockerfile\"] --\u003e A2[\"02 docker build\"] --\u003e A3[\"03 docker push\"] end subgraph P2[\"Phase 2 — CI/CD or GitOps\"] B1[\"04 Pipeline reads k8s manifests\"] --\u003e B2[\"05 kubectl apply / GitOps sync\"] end subgraph P3[\"Phase 3 — cluster control plane\"] C1[\"06 API server records state\"] --\u003e C2[\"07 Scheduler assigns node\"] end subgraph P4[\"Phase 4 — on the chosen node\"] D1[\"08 kubelet + containerd start container\"] end subgraph P5[\"Phase 5 — live traffic\"] E1[\"09 Service/Ingress routes traffic\"] end A3 --\u003e B1 B2 --\u003e C1 C2 --\u003e D1 D1 --\u003e E1 Phase 1 — your machine\nWrite a Dockerfile — instructions for building an image: base OS, install deps, copy code, entrypoint. docker build — produces a static, versioned image, a snapshot of everything needed to run the app. docker push — image is sent to a registry (Docker Hub, ECR, GCR…), tagged, e.g. myapp:a1b2c3d. Phase 2 — CI/CD or GitOps 4. A pipeline (or GitOps tool) reads your Kubernetes manifests — hand-written Deployment/Service YAML. This is a separate set of files from docker-compose.yml, not something auto-generated from it. 5. kubectl apply -f — or, more commonly in production, a GitOps tool (ArgoCD/Flux) syncing a git repo of manifests — sends the desired state (\u0026ldquo;3 replicas of this image\u0026rdquo;) to the cluster.\nPhase 3 — cluster control plane 6. The API server receives and records the desired state — every change to the cluster passes through here first. 7. The scheduler assigns each Pod to a specific node.\nPhase 4 — on the chosen node 8. The kubelet talks to the container runtime (containerd) to pull the image and start the container. This is the moment a Pod stops being a YAML wish and becomes a running process.\nPhase 5 — live traffic 9. A Service/Ingress routes real requests only to Pods that are currently healthy.\nOn docker-compose specifically: it never gets converted into a Pod automatically. Compose describes \u0026ldquo;run these containers together\u0026rdquo; for a local machine only. Kubernetes manifests describe the same kind of thing for a cluster, but are written separately — by hand, or templated with Helm. A tool called Kompose can do a rough one-time conversion, but production setups almost never rely on it.\n","date":"12 August 2026","externalUrl":null,"permalink":"/blogs/kubernetes/deployment-pipeline/","section":"Blogs","summary":"","title":"Kubernetes Deployment Pipeline (Local Machine → Production)","type":"blogs"},{"content":"A GIN index on a jsonb column makes @\u0026gt; containment queries fast by indexing every key/value pair inside the document, instead of scanning rows.\nWithout an index, WHERE data @\u0026gt; '{\u0026quot;status\u0026quot;: \u0026quot;active\u0026quot;}' on a large jsonb column forces a sequential scan. Adding CREATE INDEX ON t USING gin (data) lets Postgres use the index for containment (@\u0026gt;), existence (?), and a few other jsonb operators.\nTrade-offs:\nWrite overhead: every insert/update touching the jsonb column updates the GIN index entries for each key. Index size can be large for documents with many keys — jsonb_path_ops is a smaller, faster variant if you only need @\u0026gt;. Reference: PostgreSQL docs — GIN indexes\n","date":"11 August 2026","externalUrl":null,"permalink":"/blogs/databases/postgres-gin-indexes-for-jsonb/","section":"Blogs","summary":"","title":"Postgres GIN Indexes for JSONB Containment Queries","type":"blogs"},{"content":"EXPLAIN shows the planner\u0026rsquo;s estimated execution plan; EXPLAIN ANALYZE actually runs the query and reports real timings and row counts per step.\nEXPLAIN alone is cheap (no execution) but can be wrong when statistics are stale, which is exactly when you need real numbers most.\nEXPLAIN ANALYZE executes the query, so:\nIt\u0026rsquo;s safe for SELECTs but can be dangerous on UPDATE/DELETE — it actually runs them. Compare rows (estimated) against actual rows to spot bad cardinality estimates. A big gap between estimated and actual rows on a step is usually the first place to look when a query is slow. ","date":"10 August 2026","externalUrl":null,"permalink":"/blogs/databases/mysql-explain-analyze/","section":"Blogs","summary":"","title":"MySQL EXPLAIN ANALYZE vs EXPLAIN","type":"blogs"},{"content":"git worktree add ../foo-bugfix bugfix-branch checks out bugfix-branch into a separate directory, sharing the same .git history — no need to stash or switch branches in your main working copy.\nThis is useful when you need to run a long build or test suite on one branch while continuing to edit another, or when you want to compare behavior between two branches side by side without juggling stashes.\nRemove a worktree when you\u0026rsquo;re done with it via git worktree remove ../foo-bugfix (or git worktree prune after manually deleting the directory).\ngit worktree add ../foo-bugfix bugfix-branch git worktree list git worktree remove ../foo-bugfix ","date":"9 August 2026","externalUrl":null,"permalink":"/blogs/tools/git-worktree-basics/","section":"Blogs","summary":"","title":"git worktree Lets You Check Out Multiple Branches at Once","type":"blogs"},{"content":" SOLID Principles in Object-Oriented Design # SOLID is an acronym for five core principles of [[Object Oriented Design]] popularized by Robert C. Martin (Uncle Bob). These principles act as a roadmap for developers to create software that is easy to maintain, scale, and understand over time.\nAcronym Full Form Key Concept S Single Responsibility A class should have one reason to change. O Open-Close Software entities should be open for extension, closed for modification. L Liskov Substitution Subtypes must be substitutable for their base types. I Interface Segregation No client should be forced to depend on methods it does not use. D Dependency Inversion (DIP) Depend on abstractions, not concretions. Single Responsibility Principle (SRP) # Single responsibility states:\nA class should have one and only one reason to change, meaning that class should have only one job.\nFor example, if we consider an application where we have to calculate sum of all the areas of the given collection of shapes (square or circle).\nWe can solve this problem with below approach.\nThe Problematic Approach # Imagine you are creating AreaCalculator that handles both the math and the console output for calculating areas of all of the available shapes.\npublic interface Shape { } public class Square implements Shape { public double length; public Square(double length) { this.length = length; } } public class Circle implements Shape { public double radius; public Circle(double radius) { this.radius = radius; } } public class AreaCalculator { private final Shape[] shapes; public AreaCalculator(Shape[] shapes) { this.shapes = shapes; } public double sum() { return Arrays.stream(shapes) .mapToDouble(shape -\u0026gt; { if (shape instanceof Circle) { return Math.PI * Math.pow(((Circle) shape).radius, 2); } else if (shape instanceof Square) { return Math.pow(((Square) shape).length, 2); } return 0.0; }) .sum(); } public void output() { System.out.println(\u0026#34;Sum of areas of shape provided = \u0026#34; + sum()); } } In this example there are two places where we are violating Single Responsibility Principle.\nMethod Sum : This sum method has two responsibilities - Calculation of all the areas based on type of shape and Summing all the areas.\nProblem: If we have to add new shape in the program then we will have to update this method which violates SRP. 2. Output:\nOutput of this class is strictly console based.\nWhat if we need to change this output to non-console, something like JSON or HTML, again this class needs to be modified.\nSolution: # Create Square and Circle shapes but with added responsibility of calculating there own area. public interface Shape { double calculateArea(); } public class Circle implements Shape { public double radius; public Circle(double radius) { this.radius = radius; } @Override public double calculateArea() { return Math.PI * Math.pow(this.radius, 2); } } public class Square implements Shape { public double length; public Square(double length) { this.length = length; } @Override public double calculateArea() { return Math.pow(this.length, 2); } } With the delegation of area calculation above the AreaCalculator class will have just one responsibility of calculating the sum of all the areas, where each area will be provided by the shape class itself. public class AreaCalculator { private final Shape[] shapes; public AreaCalculator(Shape[] shapes) { this.shapes = shapes; } public double sum() { return Arrays.stream(shapes) .mapToDouble(Shape::calculateArea) .sum(); } } Now the question arises, what about output. Output we will be handling differently using different class called AreaCalculatorOutputter whose sole responsibility will be to create output in different format by invoking sum method in AreaCalculator.\npublic class AreaCalculatorOutputter { private final AreaCalculator areaCalculator; public AreaCalculatorOutputter(AreaCalculator areaCalculator) { this.areaCalculator = areaCalculator; } public String jsonOutput() { double sum = this.areaCalculator.sum(); return String.format(\u0026#34;{\\\u0026#34;total_area\\\u0026#34;: \\\u0026#34;Total area for the shapes provided is : [%s]\\\u0026#34;}\u0026#34;, sum); } public String htmlOutput() { double sum = this.areaCalculator.sum(); return String.format(\u0026#34;\u0026lt;div\u0026gt;Total area for the shapes provided is : [%s]\u0026lt;/div\u0026gt;\u0026#34;, sum); } } Open-Closed Principle (OCP) # Open Close principle states:\nThe class should be open for extension but closed for modification.\nWhat does this means? In the original AreaCalculator, adding a new shape (like a Triangle) requires modifying the sum() method with a new if statement. This makes the class fragile.\npublic class AreaCalculator { private final Shape[] shapes; public AreaCalculator(Shape[] shapes) { this.shapes = shapes; } public double sum() { return Arrays.stream(shapes) .mapToDouble(shape -\u0026gt; { if (shape instanceof Circle) { return Math.PI * Math.pow(((Circle) shape).radius, 2); } else if (shape instanceof Square) { return Math.pow(((Square) shape).length, 2); } return 0.0; }) .sum(); } public void output() { System.out.println(\u0026#34;Sum of areas of shape provided = \u0026#34; + sum()); } } In this case what will happen? If you want to add another Triangle shape in the array it won’t calculate the area unless you change the code to have 3rd scenario.\nBut about pentagon or a rhombus or a parallelogram?\nIrrespective of whatever number of shapes you will add to the collection the class has to be modified.\nThis means this class is definitely not open for extension and absolutely not close to modification.\nThe Solution By using the Shape interface with a calculateArea() method, the AreaCalculator becomes closed to modification. You can add any number of new shapes without changing the calculator\u0026rsquo;s code.\npublic class AreaCalculator { private final Shape[] shapes; public AreaCalculator(Shape[] shapes) { this.shapes = shapes; } public double sum() { return Arrays.stream(shapes) .mapToDouble(Shape::calculateArea) .sum(); } } Now irrespective of what shape you add to the collection, if it is of type shape the total area will be calculated.\nLiskov Substitution # Liskov Substitution principle states that\nObjects of a superclass (parent) should be replaceable with objects of its subclasses (children) without breaking the application.\nWhat does this means? This means when inheriting code from parent, the child class must also honor the behavior and expectations of parent.\nFor example [The Classic Violation: Square vs. Rectangle] If we create Rectangle and Square class where Square extends Rectangle.\npublic class Rectangle { private int width; private int length; public int getWidth() { return width; } public void setWidth(int width) { this.width = width; } public int getLength() { return length; } public void setLength(int length) { this.length = length; } public int getArea() { return this.width * this.length; } } public class Square extends Rectangle { @Override public void setWidth(int width) { super.setWidth(width); super.setLength(width); } @Override public void setLength(int length) { super.setLength(length); super.setWidth(length); } } In this case as long as Rectangle is used everything is fine. But in cases where Rectangle refers Square and when you calculate area the answer can be wrong. Look at the below example.\npublic class Runner { public static void main(String[] args) { Rectangle r = new Rectangle(); r.setLength(10); r.setWidth(5); assert 50 == r.getArea() : \u0026#34;Area should be 50\u0026#34;; Rectangle s = new Square(); s.setWidth(5); s.setLength(10); assert 50 == s.getArea() : \u0026#34;Area should be 50\u0026#34;; System.out.println(\u0026#34;Completed\u0026#34;); } } In the above example if you run the first Rectangle will give you correct area whereas the second Rectangle which represents Square will have result in wrong output because it violates LSP.\nThe Solution: Instead of forced inheritance, treat Square and Rectangle as separate implementations of a Shape interface to ensure calculateArea(Shape s) works predictably for both.\npublic interface Shape { double getArea(); } public class Rectangle implements Shape { private int length; private int width; public int getLength() { return length; } public void setLength(int length) { this.length = length; } public int getWidth() { return width; } public void setWidth(int width) { this.width = width; } @Override public double getArea() { return 0; } } public class Square implements Shape { private int side; public int getSide() { return side; } public void setSide(int side) { this.side = side; } @Override public double getArea() { return Math.pow(this.side, 2); } } public class Runner { public static void main(String[] args) { Rectangle r = new Rectangle(); r.setLength(10); r.setWidth(5); assert 50 == calculateArea(r) : \u0026#34;Area should be 50\u0026#34;; Square s = new Square(); s.setSide(10); assert 100 == calculateArea(s) : \u0026#34;Area should be 100\u0026#34;; System.out.println(\u0026#34;All completed\u0026#34;); } private static double calculateArea(Shape s) { return s.getArea(); } } Interface Segregation # The interface segregation principle states:\nA class should never be forced to implement methods it doesn’t use.\nWhat does it mean? We should always try to avoid “FAT INTERFACE PROBLEM”, meaning, we should not create interface that does everything, and the result of that all implementors are forced to implement methods which are not of there use.\nFor example:\npublic interface SmartDevice { void print(); void scan(); void fax(); } public class BasicPrinter implements SmartDevice { @Override public void print() { System.out.println(\u0026#34;The printer is Printing...\u0026#34;); } @Override public void scan() { throw new UnsupportedOperationException(\u0026#34;This operation is not supported in Basic Printer\u0026#34;); } @Override public void fax() { throw new UnsupportedOperationException(\u0026#34;This operation is not supported in Basic Printer\u0026#34;); } } public class NewAgePrinter implements SmartDevice { @Override public void print() { System.out.println(\u0026#34;The printer is printing...\u0026#34;); } @Override public void scan() { System.out.println(\u0026#34;The printer is scanning...\u0026#34;); } @Override public void fax() { System.out.println(\u0026#34;The printer is sending the fax...\u0026#34;); } } In the above example the SmartDevice interface is the best example of Fat Interface Problem.\nThis class includes everything, it does not values the fact that there might be printer instance which cannot send fax or do scanning, as in case of BasicPrinter.\nThis kind of implementation is prone to errors like:\nConfusion: A developer using BasicPrinter might think it works for Scanning and Faxing, due to autocomplete feature of many modern IDEs. But during the runtime it will come to know that these features are not supported in BasicPrinter. This might lead to crash during runtime as well. Rigidity: If there is a change in Fax method signature, the BasicPrinter class also needs to be changed even though it does not work do Fax. Deployment issues: in large systems changing a fat interface forces a re-compilation of every class that uses it, even if they don’t care about the specific method you changed. The Solution: Break the interface into smaller, specific contracts like Printer, Scanner, and FaxMachine.\npublic interface Printer { void print(); } public interface Scanner { void scan(); } public interface FaxMachine { void fax(); } public class BasicPrinter implements Printer { @Override public void print() { System.out.println(\u0026#34;The printer is printing...\u0026#34;); } } public class NewAgePrinter implements Printer, Scanner, FaxMachine{ @Override public void fax() { System.out.println(\u0026#34;Machine is sending fax...\u0026#34;); } @Override public void print() { System.out.println(\u0026#34;Printer is printing...\u0026#34;); } @Override public void scan() { System.out.println(\u0026#34;Scanner is scanning...\u0026#34;); } } In this implementation all the classes implements exactly what they need to work with.\nthe BasicPrinter only prints stuff and the NewAgePrinter does all the work like printing, scanning and sending fax.\nDependency Inversion # Dependency Inversion states:\nHigh-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces). Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions. What does this means? Imagine you are building a NotificationManager, for now you want to send email.\npublic class EmailSender { public void send(String message) { System.out.println(\u0026#34;Sending email : \u0026#34; + message); } } public class NotificationManager { private EmailSender emailSender = new EmailSender(); public void notify(String message) { emailSender.send(message); } } In this there are three major problems:\nRigid: NotificationManager is very rigid in nature, if you want to send SMS instead of email, you have to change the class to do it.\nUntestable: NotificationManager is not testable on it own. If the functionality of notification manager has to be tested then the email must be sent to do it.\nViolates OCP: If there is a requirement change and user wants to add new notification type the existing code must be modified.\nSolution:\npublic interface MessageService { void sendMessage(String message); } public class EmailService implements MessageService { @Override public void sendMessage(String message) { System.out.println(\u0026#34;Sending message: \u0026#34; + message + \u0026#34;, via email...\u0026#34;); } } public class SmsService implements MessageService { @Override public void sendMessage(String message) { System.out.println(\u0026#34;Sending message: \u0026#34; + message + \u0026#34;, via SMS.\u0026#34;); } } public class NotificationManager { private final MessageService messageService; public NotificationManager(MessageService messageService) { this.messageService = messageService; } public void notify(String message) { this.messageService.sendMessage(message); } } With this now NotificationManager doesn’t directly depend on any kind of implementation rather it takes the contract and execute type of implementation whatever is provided at the runtime.\n","date":"2 January 2026","externalUrl":null,"permalink":"/blogs/basics/solid-principle/","section":"Blogs","summary":"","title":"","type":"blogs"},{"content":" Core Concepts # Solid Principle # How can you choose right collection for your work in Java # What Really Happens When You Add @SpringBootApplication? # Spring Boot Core Concepts # Spring WebFlux and Reactive Programming # WebSockets in Java: Threading, Scaling, and Virtual Threads # System Design # Back-of-the-Envelope Estimation # Architecture # Domain-Driven Design Basics # Microservices Architecture Fundamentals # Event-Driven Architecture Patterns # Dependency Injection and Inversion of Control # Kafka # Apache Kafka Concepts # Kubernetes # Kubernetes Deployment Pipeline (Local Machine → Production) # Databases # MySQL EXPLAIN ANALYZE vs EXPLAIN # Postgres GIN Indexes for JSONB Containment Queries # Tooling # git worktree Lets You Check Out Multiple Branches at Once # AI/LLM # AI-Assisted Development and Agentic Workflows # Changes In # Java # From 11 to 17 # From 17 to 21 # ","date":"2 February 2024","externalUrl":null,"permalink":"/posts/","section":"Amar Singh","summary":"","title":"","type":"page"},{"content":"Before you draw a single box in a system design, you need a rough sense of scale — how many users, how many requests per second, how much data you\u0026rsquo;re storing, how much bandwidth you need. Back-of-the-envelope estimation is the practice of turning a few known numbers (users, activity, message size) into these scale numbers using simple arithmetic. It doesn\u0026rsquo;t need to be precise; it needs to tell you whether you\u0026rsquo;re building something that fits on one server or something that needs to be sharded across a thousand.\nThe technique is always the same shape: start from user metrics you\u0026rsquo;re given or can reasonably assume, derive request/message rates from them, then derive storage and bandwidth from those rates. Each stage feeds the next, so a mistake or a changed assumption early on ripples through everything downstream — which is exactly why it helps to make the calculation live instead of doing it once on paper.\nBelow is a worked example for a WhatsApp-style messaging service. Change any of the input numbers and everything downstream recalculates automatically.\nUser metrics Registered users Daily active users (%) Avg messages per user / day Peak-hour multiplier Message estimations Daily active users \u0026ndash; registeredUsers * (dauPercent / 100) Daily messages \u0026ndash; dau * msgsPerUserPerDay Avg messages / sec \u0026ndash; dailyMessages / 86400 Peak messages / sec \u0026ndash; avgMsgsPerSec * peakMultiplier Storage calculation Bytes per message (with metadata) Daily storage \u0026ndash; dailyMessages * bytesPerMessage Annual storage \u0026ndash; dailyStorage * 365 Bandwidth Concurrent connections at peak Bytes/sec per active connection Peak bandwidth \u0026ndash; concurrentConnections * bytesPerSecPerConnection A few things worth noticing about the chain above:\nMessage estimations derives everything from registeredUsers and dauPercent — bump the DAU percentage and both the daily message count and the peak throughput move with it. Storage calculation reaches back into dailyMessages from the section above it, rather than recomputing it — this is what makes the sections composable instead of a wall of duplicated formulas. Peak-hour multiplier is a judgment call (3-5x average is a common rule of thumb for chat/social traffic), not a measured number — estimation is as much about naming your assumptions explicitly as it is about the arithmetic. None of these numbers need to be exact. What matters is landing in the right order of magnitude — whether storage is measured in gigabytes, terabytes, or petabytes changes the entire architecture, and that\u0026rsquo;s the question this kind of estimation is meant to answer.\n","externalUrl":null,"permalink":"/blogs/system-design/back-of-the-envelope-estimation/","section":"Blogs","summary":"","title":"Back-of-the-Envelope Estimation for System Design","type":"blogs"},{"content":"Choosing the right Java collection can feel overwhelming. The standard library offers dozens of options across maps, sets, lists, and queues, each with different performance characteristics, ordering guarantees, and thread-safety trade-offs. Get it wrong and you are looking at subtle bugs, unexpected memory leaks, or performance bottlenecks under load. To cut through the noise, the decision tree below walks you through every major collection in the JDK, asking the right questions at each step so you always land on the best tool for your specific scenario.\n%%{init: {'flowchart': {'useMaxWidth': true}}}%% flowchart LR START([What do you need to store?]) START --\u003e KV{Key-value pairs?} %% ── MAP BRANCH ────────────────────────────── KV -- Yes --\u003e THREAD_MAP{Thread-safe?} THREAD_MAP -- Yes --\u003e CHM[ConcurrentHashMap Lock-striped, high concurrency] THREAD_MAP -- No --\u003e ORDER_MAP{Order / sorting needed?} ORDER_MAP -- Sorted keys --\u003e TM[TreeMapNavigableMap, sorted by key] ORDER_MAP -- Insertion order --\u003e LHM[LinkedHashMapPredictable iteration order] ORDER_MAP -- No order needed --\u003e HM{Key type?} HM -- Enum keys --\u003e EM[EnumMapFastest map for enum keys] HM -- Identity == --\u003e IHM[IdentityHashMapUses == not .equals] HM -- Weak refs --\u003e WHM[WeakHashMapEntries GC-eligible] HM -- General --\u003e HMP[HashMapFastest general-purpose map] %% ── COLLECTION BRANCH ─────────────────────── KV -- No --\u003e DUPES{Allow duplicates?} %% SET sub-branch DUPES -- No, unique only --\u003e SET_THREAD{Thread-safe?} SET_THREAD -- Yes --\u003e COWAS[CopyOnWriteArraySetThread-safe, small sets] SET_THREAD -- No --\u003e SET_ORDER{Order / sorting?} SET_ORDER -- Sorted --\u003e TS[TreeSetNavigableSet, sorted] SET_ORDER -- Insertion order --\u003e LHS[LinkedHashSetPredictable iteration] SET_ORDER -- Enum values --\u003e ES[EnumSetFastest set for enums] SET_ORDER -- No order --\u003e HS[HashSetFastest general-purpose set] %% LIST / QUEUE sub-branch DUPES -- Yes, duplicates OK --\u003e BEHAVIOR{Access pattern?} BEHAVIOR -- Indexed list --\u003e THREAD_LIST{Thread-safe?} BEHAVIOR -- Queue / Stack --\u003e QS{FIFO, LIFO, or Priority?} THREAD_LIST -- Yes, legacy --\u003e VEC[Vector / StackLegacy, synchronized] THREAD_LIST -- Yes, modern --\u003e COWAL[CopyOnWriteArrayListBest for read-heavy] THREAD_LIST -- No, frequent get --\u003e AL[ArrayListFast random access] THREAD_LIST -- No, frequent insert/delete --\u003e LL[LinkedListFast head/tail ops] QS -- FIFO, no priority --\u003e BQ{Thread-safe?} QS -- FIFO + priority --\u003e PQ[PriorityQueueMin-heap, natural order] QS -- LIFO stack --\u003e ADS[ArrayDeque as StackPreferred over Stack class] QS -- Bounded / blocking --\u003e BLK[ArrayBlockingQueueLinkedBlockingQueueProducer-consumer] BQ -- Yes --\u003e BQ2[LinkedBlockingQueue] BQ -- No --\u003e BQ3[ArrayDequeFastest general queue] %% ── STYLING ────────────────────────────────── classDef question fill:#E6F1FB,stroke:#185FA5,color:#0C447C classDef map fill:#E1F5EE,stroke:#0F6E56,color:#085041 classDef setcls fill:#EEEDFE,stroke:#534AB7,color:#3C3489 classDef list fill:#E1F5EE,stroke:#0F6E56,color:#085041 classDef queue fill:#FAEEDA,stroke:#854F0B,color:#633806 classDef legacy fill:#F1EFE8,stroke:#5F5E5A,color:#444441 classDef conc fill:#FAECE7,stroke:#993C1D,color:#712B13 class KV,ORDER_MAP,HM,SET_ORDER,BEHAVIOR,QS,THREAD_MAP,THREAD_LIST,SET_THREAD,BQ,DUPES question class TM,LHM,HMP,EM,IHM,WHM map class TS,LHS,HS,ES setcls class AL,LL list class PQ,ADS,BQ3,BQ2 queue class BLK,CHM,COWAL,COWAS conc class VEC legacy ","externalUrl":null,"permalink":"/blogs/java/which-collection-use/","section":"Blogs","summary":"","title":"How to choose right collection in Java?","type":"blogs"},{"content":" Java 12 # Switch Expressions (Preview) JEP 325 # More on this in Switch Expression\nJava 13 # Switch Expressions (Second Preview) JEP 354 # More on this in Switch Expression\nJava 14 # Switch Expression # Below are the changes in the switch construct introduced in Java 14, formalized by JEP 361, JEP-354, and JEP-325. The new Switch Expression addresses three main issues with the traditional switch statement: verbosity, variable scope issues, and being limited to a statement.\n1. Its unnecessarily verbose and error prone # The traditional switch required break statements, leading to repetitive code and the potential for fall-through errors if a break was missed.\nProblem Example (Traditional Switch Statement):\nswitch (day) { case MONDAY: case FRIDAY: case SUNDAY: System.out.println(6); break; case TUESDAY: System.out.println(7); break; case THURSDAY: case SATURDAY: System.out.println(8); break; case WEDNESDAY: System.out.println(9); break; } Solution (Switch Expression with -\u0026gt;):\nThe new syntax uses case L -\u0026gt;, which eliminates the need for an explicit break and allows multiple labels to be grouped.\nswitch (weekDay) { case MONDAY, FRIDAY, SUNDAY -\u0026gt; System.out.println(6); case TUESDAY -\u0026gt; System.out.println(7); case THURSDAY, SATURDAY -\u0026gt; System.out.println(8); case WEDNESDAY -\u0026gt; System.out.println(9); } 2. Scope of variable within Switch # A variable declared in one case block in the traditional switch is scoped to the entire switch block, causing errors if the same variable name is used in another arm.\nProblem Example (Variable Scope):\nswitch (day) { case MONDAY: case TUESDAY: int temp = ...; break; case WEDNESDAY: case THURSDAY: int temp = ...; // Can\u0026#39;t call this variable \u0026#39;temp\u0026#39; break; default: int temp = ...; // Can\u0026#39;t call this variable \u0026#39;temp\u0026#39; } Error: Variable \u0026rsquo;temp\u0026rsquo; is already defined in the scope\nWorkaround: The common workaround was declaring a variable outside the switch and assigning values inside each case.\nint temp; switch (day) { case MONDAY: case FRIDAY: case SUNDAY: temp = 6; break; case TUESDAY: temp = 7; break; case THURSDAY: case SATURDAY: temp = 8; break; case WEDNESDAY: temp = 9; break; default: throw new IllegalStateException(\u0026#34;Wat: \u0026#34; + day); } Solution: Using yield or Expression Form: The new switch can return a value, naturally handling scope.\nUsing yield (with :): The yield keyword is used to produce a value from the switch expression. Integer temp = switch (weekDay) { case MONDAY, FRIDAY, SUNDAY: yield 6; case TUESDAY: yield 7; case THURSDAY, SATURDAY: yield 8; case WEDNESDAY: yield 9; }; Using -\u0026gt; (as expression): This concise syntax implicitly yields the result. Integer temp = switch (weekDay) { case MONDAY, FRIDAY, SUNDAY -\u0026gt; 6; case TUESDAY -\u0026gt; 7; case THURSDAY, SATURDAY -\u0026gt; 8; case WEDNESDAY -\u0026gt; 9; }; 3. Limited to a statement not expression # The traditional switch was limited to a statement and could not compute and return a single value directly.\nAs Statement (Original):\nint numLetters; switch (day) { case MONDAY: case FRIDAY: case SUNDAY: numLetters = 6; System.out.println(6); break; case TUESDAY: numLetters = 7; System.out.println(6); break; case THURSDAY: case SATURDAY: numLetters = 8; System.out.println(6); break; case WEDNESDAY: numLetters = 9; System.out.println(6); break; default: throw new IllegalStateException(\u0026#34;Wat: \u0026#34; + day); } As Expression (New Feature): The switch can now be used as an expression, where its result can be directly used in a statement like System.out.println().\nSystem.out.println( switch (weekDay) { case MONDAY, FRIDAY, SUNDAY -\u0026gt; 6; case TUESDAY -\u0026gt; 7; case THURSDAY, SATURDAY -\u0026gt; 8; case WEDNESDAY -\u0026gt; 9; } ); Java 15 # Java 16 # Java 17 # Some important links # JEP Link JEP-354 https://openjdk.org/jeps/354 JEP-325 https://openjdk.org/jeps/325 JEP 361 https://openjdk.org/jeps/361 ","externalUrl":null,"permalink":"/blogs/java/11-to-17/","section":"Blogs","summary":"","title":"Things changed from Java 11 - 17","type":"blogs"},{"content":"Every Spring Boot app starts with the same line of ceremony: a main method, a call to SpringApplication.run(...), and a single annotation — @SpringBootApplication — sitting on top of the class. It looks like magic, but it isn\u0026rsquo;t one thing at all. It\u0026rsquo;s three annotations wearing a trench coat, and understanding what each of them does is the fastest way to stop being surprised by Spring Boot.\nIt\u0026rsquo;s a Meta-Annotation # If you open up the Spring Boot source, @SpringBootApplication is declared roughly like this:\n@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication { // ... } So putting @SpringBootApplication on your main class is exactly equivalent to stacking these three annotations yourself:\n%%{init: {'flowchart': {'useMaxWidth': true}}}%% flowchart LR SBA([\"@SpringBootApplication\"]) SBA --\u003e SBC[\"@SpringBootConfigurationMarks this class as a sourceof bean definitions (it's aspecialised @Configuration)\"] SBA --\u003e EAC[\"@EnableAutoConfigurationGuesses \u0026 configures beansbased on what's on the classpath\"] SBA --\u003e CS[\"@ComponentScanScans this package (and below)for @Component-annotated classes\"] classDef root fill:#E6F1FB,stroke:#185FA5,color:#0C447C classDef cfg fill:#E1F5EE,stroke:#0F6E56,color:#085041 classDef auto fill:#FAEEDA,stroke:#854F0B,color:#633806 classDef scan fill:#EEEDFE,stroke:#534AB7,color:#3C3489 class SBA root class SBC cfg class EAC auto class CS scan Two of these are simple. The third is where all the interesting behavior lives.\n@ComponentScan — Finding Your Beans # @ComponentScan tells Spring where to look for classes annotated with @Component, @Service, @Repository, and @Controller. By default, it scans the package that the annotated class lives in, plus every sub-package.\nThis is exactly why the convention is to put your @SpringBootApplication class at the root package of your project (e.g. com.example.app, with everything else nested underneath). If you moved it into a leaf package, sibling packages would silently fall outside the scan and their beans would never be registered.\n@EnableAutoConfiguration — The Part That Feels Like Magic # This is the annotation that lets you add spring-boot-starter-web to your pom.xml, write zero configuration, and still get an embedded Tomcat server with sensible defaults. Here\u0026rsquo;s the mechanism behind that:\nAt startup, Spring Boot\u0026rsquo;s auto-configuration import selector reads a list of candidate configuration classes from META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (bundled inside spring-boot-autoconfigure.jar; older versions used META-INF/spring.factories for the same purpose). Every one of those candidates is itself an @AutoConfiguration class — but almost none of them are unconditionally applied. Each is guarded by one or more @Conditional... annotations: @ConditionalOnClass (does this class exist on the classpath?), @ConditionalOnMissingBean (have you already defined your own?), @ConditionalOnProperty (is a config flag set?), and more. So DispatcherServletAutoConfiguration only activates because spring-boot-starter-web put DispatcherServlet on your classpath. If you define your own DataSource bean, DataSourceAutoConfiguration backs off entirely because of @ConditionalOnMissingBean. Nothing is truly automatic — it\u0026rsquo;s a big set of \u0026ldquo;if this, then that\u0026rdquo; rules evaluated once, at startup.\n%%{init: {'flowchart': {'useMaxWidth': true}}}%% flowchart TD A[\"SpringApplication.run(Main.class, args)\"] --\u003e B[Create ApplicationContext] B --\u003e C[\"@ComponentScan registers your@Component / @Service / @Repository / @Controller beans\"] C --\u003e D[\"Auto-configuration import selector loads candidates fromAutoConfiguration.imports\"] D --\u003e E{\"Each candidate's@Conditional... annotationsare evaluated\"} E -- \"Condition matchese.g. class found on classpath\" --\u003e F[Auto-config class registers its beans] E -- \"Condition failse.g. you already defined that bean\" --\u003e G[Auto-config class is skipped] F --\u003e H[Context refresh completes] G --\u003e H H --\u003e I([Application is ready]) classDef start fill:#E6F1FB,stroke:#185FA5,color:#0C447C classDef scan fill:#EEEDFE,stroke:#534AB7,color:#3C3489 classDef auto fill:#FAEEDA,stroke:#854F0B,color:#633806 classDef applied fill:#E1F5EE,stroke:#0F6E56,color:#085041 classDef skipped fill:#F1EFE8,stroke:#5F5E5A,color:#444441 classDef done fill:#FAECE7,stroke:#993C1D,color:#712B13 class A,B start class C scan class D,E auto class F applied class G skipped class H,I done Why This Design Matters # This is Spring Boot\u0026rsquo;s version of \u0026ldquo;convention over configuration\u0026rdquo;: instead of you wiring up a DataSource, a DispatcherServlet, or a JacksonObjectMapper by hand, Spring Boot ships hundreds of pre-written @AutoConfiguration classes that quietly check \u0026ldquo;does this apply here?\u0026rdquo; and wire themselves in only when it makes sense. @SpringBootApplication is just the single switch that turns all three of these mechanisms — configuration, auto-configuration, and component scanning — on at once, which is exactly why removing it (or splitting it back into its three parts) is a perfectly valid, and sometimes clearer, thing to do once you understand what each piece is actually doing.\n","externalUrl":null,"permalink":"/blogs/spring/spring-boot-application-annotation/","section":"Blogs","summary":"","title":"What Really Happens When You Add @SpringBootApplication?","type":"blogs"},{"content":"","date":"17 September 2026","externalUrl":null,"permalink":"/","section":"Amar Singh","summary":"","title":"Amar Singh","type":"page"},{"content":"","date":"17 September 2026","externalUrl":null,"permalink":"/blogs/","section":"Blogs","summary":"","title":"Blogs","type":"blogs"},{"content":"","date":"17 September 2026","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"17 September 2026","externalUrl":null,"permalink":"/tags/distributed-systems/","section":"Tags","summary":"","title":"Distributed-Systems","type":"tags"},{"content":"","date":"17 September 2026","externalUrl":null,"permalink":"/tags/event-driven-architecture/","section":"Tags","summary":"","title":"Event-Driven-Architecture","type":"tags"},{"content":"","date":"17 September 2026","externalUrl":null,"permalink":"/tags/exactly-once/","section":"Tags","summary":"","title":"Exactly-Once","type":"tags"},{"content":"","date":"17 September 2026","externalUrl":null,"permalink":"/categories/kafka/","section":"Categories","summary":"","title":"Kafka","type":"categories"},{"content":"","date":"17 September 2026","externalUrl":null,"permalink":"/tags/kafka/","section":"Tags","summary":"","title":"Kafka","type":"tags"},{"content":"","date":"17 September 2026","externalUrl":null,"permalink":"/tags/kafka-connect/","section":"Tags","summary":"","title":"Kafka-Connect","type":"tags"},{"content":"","date":"17 September 2026","externalUrl":null,"permalink":"/tags/kafka-streams/","section":"Tags","summary":"","title":"Kafka-Streams","type":"tags"},{"content":"","date":"17 September 2026","externalUrl":null,"permalink":"/tags/messaging/","section":"Tags","summary":"","title":"Messaging","type":"tags"},{"content":"","date":"17 September 2026","externalUrl":null,"permalink":"/tags/schema-registry/","section":"Tags","summary":"","title":"Schema-Registry","type":"tags"},{"content":"","date":"17 September 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"14 September 2026","externalUrl":null,"permalink":"/tags/agentic-workflows/","section":"Tags","summary":"","title":"Agentic-Workflows","type":"tags"},{"content":"","date":"14 September 2026","externalUrl":null,"permalink":"/tags/ai/","section":"Tags","summary":"","title":"Ai","type":"tags"},{"content":"","date":"14 September 2026","externalUrl":null,"permalink":"/categories/ai/llm/","section":"Categories","summary":"","title":"AI/LLM","type":"categories"},{"content":"","date":"14 September 2026","externalUrl":null,"permalink":"/tags/claude-api/","section":"Tags","summary":"","title":"Claude-Api","type":"tags"},{"content":"","date":"14 September 2026","externalUrl":null,"permalink":"/tags/copilot/","section":"Tags","summary":"","title":"Copilot","type":"tags"},{"content":"","date":"14 September 2026","externalUrl":null,"permalink":"/tags/ddd/","section":"Tags","summary":"","title":"Ddd","type":"tags"},{"content":"","date":"14 September 2026","externalUrl":null,"permalink":"/tags/dependency-injection/","section":"Tags","summary":"","title":"Dependency-Injection","type":"tags"},{"content":"","date":"14 September 2026","externalUrl":null,"permalink":"/categories/distributed-systems/","section":"Categories","summary":"","title":"Distributed Systems","type":"categories"},{"content":"","date":"14 September 2026","externalUrl":null,"permalink":"/tags/domain-driven-design/","section":"Tags","summary":"","title":"Domain-Driven-Design","type":"tags"},{"content":"","date":"14 September 2026","externalUrl":null,"permalink":"/categories/java/","section":"Categories","summary":"","title":"Java","type":"categories"},{"content":"","date":"14 September 2026","externalUrl":null,"permalink":"/tags/java/","section":"Tags","summary":"","title":"Java","type":"tags"},{"content":"","date":"14 September 2026","externalUrl":null,"permalink":"/tags/llm/","section":"Tags","summary":"","title":"Llm","type":"tags"},{"content":"","date":"14 September 2026","externalUrl":null,"permalink":"/tags/microservices/","section":"Tags","summary":"","title":"Microservices","type":"tags"},{"content":"","date":"14 September 2026","externalUrl":null,"permalink":"/tags/reactive-programming/","section":"Tags","summary":"","title":"Reactive-Programming","type":"tags"},{"content":"","date":"14 September 2026","externalUrl":null,"permalink":"/tags/spring/","section":"Tags","summary":"","title":"Spring","type":"tags"},{"content":"","date":"14 September 2026","externalUrl":null,"permalink":"/tags/spring-boot/","section":"Tags","summary":"","title":"Spring-Boot","type":"tags"},{"content":"","date":"14 September 2026","externalUrl":null,"permalink":"/tags/system-design/","section":"Tags","summary":"","title":"System-Design","type":"tags"},{"content":"","date":"2 September 2026","externalUrl":null,"permalink":"/tags/concurrency/","section":"Tags","summary":"","title":"Concurrency","type":"tags"},{"content":"","date":"2 September 2026","externalUrl":null,"permalink":"/tags/threading/","section":"Tags","summary":"","title":"Threading","type":"tags"},{"content":"","date":"2 September 2026","externalUrl":null,"permalink":"/tags/virtual-threads/","section":"Tags","summary":"","title":"Virtual-Threads","type":"tags"},{"content":"","date":"2 September 2026","externalUrl":null,"permalink":"/tags/websockets/","section":"Tags","summary":"","title":"Websockets","type":"tags"},{"content":"","date":"25 August 2026","externalUrl":null,"permalink":"/tags/design-patterns/","section":"Tags","summary":"","title":"Design-Patterns","type":"tags"},{"content":"","date":"25 August 2026","externalUrl":null,"permalink":"/tags/inversion-of-control/","section":"Tags","summary":"","title":"Inversion-of-Control","type":"tags"},{"content":"","date":"12 August 2026","externalUrl":null,"permalink":"/tags/ci-cd/","section":"Tags","summary":"","title":"Ci-Cd","type":"tags"},{"content":"","date":"12 August 2026","externalUrl":null,"permalink":"/tags/deployment/","section":"Tags","summary":"","title":"Deployment","type":"tags"},{"content":"","date":"12 August 2026","externalUrl":null,"permalink":"/categories/devops/","section":"Categories","summary":"","title":"DevOps","type":"categories"},{"content":"","date":"12 August 2026","externalUrl":null,"permalink":"/tags/docker/","section":"Tags","summary":"","title":"Docker","type":"tags"},{"content":"","date":"12 August 2026","externalUrl":null,"permalink":"/tags/kubernetes/","section":"Tags","summary":"","title":"Kubernetes","type":"tags"},{"content":"","date":"11 August 2026","externalUrl":null,"permalink":"/categories/databases/","section":"Categories","summary":"","title":"Databases","type":"categories"},{"content":"","date":"11 August 2026","externalUrl":null,"permalink":"/tags/indexing/","section":"Tags","summary":"","title":"Indexing","type":"tags"},{"content":"","date":"11 August 2026","externalUrl":null,"permalink":"/tags/jsonb/","section":"Tags","summary":"","title":"Jsonb","type":"tags"},{"content":"","date":"11 August 2026","externalUrl":null,"permalink":"/tags/postgres/","section":"Tags","summary":"","title":"Postgres","type":"tags"},{"content":"","date":"10 August 2026","externalUrl":null,"permalink":"/tags/mysql/","section":"Tags","summary":"","title":"Mysql","type":"tags"},{"content":"","date":"10 August 2026","externalUrl":null,"permalink":"/tags/performance/","section":"Tags","summary":"","title":"Performance","type":"tags"},{"content":"","date":"9 August 2026","externalUrl":null,"permalink":"/tags/git/","section":"Tags","summary":"","title":"Git","type":"tags"},{"content":"","date":"9 August 2026","externalUrl":null,"permalink":"/categories/tooling/","section":"Categories","summary":"","title":"Tooling","type":"categories"},{"content":"[[Object Oriented Design Guidelines]]\n","externalUrl":null,"permalink":"/blogs/basics/dry/","section":"Blogs","summary":"","title":"","type":"blogs"},{"content":"","externalUrl":null,"permalink":"/blogs/java/17-to-21/","section":"Blogs","summary":"","title":"","type":"blogs"},{"content":"","externalUrl":null,"permalink":"/blogs/java/lts-to-lts-timeline/","section":"Blogs","summary":"","title":"","type":"blogs"},{"content":"","externalUrl":null,"permalink":"/blogs/spring/4-to-5/","section":"Blogs","summary":"","title":"","type":"blogs"},{"content":"","externalUrl":null,"permalink":"/blogs/spring/5-to-6/","section":"Blogs","summary":"","title":"","type":"blogs"},{"content":"","externalUrl":null,"permalink":"/blogs/spring/boot-2-to-3/","section":"Blogs","summary":"","title":"","type":"blogs"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]