Showing posts with label Backend Engineering. Show all posts
Showing posts with label Backend Engineering. Show all posts

How Python Code Works Internally (Step-by-Step)

 


Many developers say “Python is an interpreted language”, but internally Python follows a compile + interpret model.

Let’s break it down clearly using a real example and the same pipeline shown in the diagram.


🔷 High-Level Python Execution Flow

Source Code (.py) ↓ Compiler ↓ Bytecode (.pyc) ↓ Python Virtual Machine (PVM) ↓ Running Program

🧩 Step 1: Python Source Code

This is the code we write.

Example: hello.py

x = 10 y = 20 print(x + y)

This is human-readable, not machine-readable.


🧩 Step 2: Compiler (Inside Python Interpreter)

When you run:

python hello.py

Python does NOT execute the source code directly.

What the compiler does:

  • Checks syntax

  • Converts source code into bytecode

  • Bytecode is platform-independent

📌 This compilation happens automatically — you never run a compiler manually.


🧩 Step 3: Bytecode (.pyc)

After compilation:

  • Python generates bytecode

  • Stored in __pycache__ directory

Example:

__pycache__/ hello.cpython-311.pyc

Bytecode characteristics:

✔ Low-level instructions
✔ Optimized for execution
✔ Same across Windows / Mac / Linux


🧩 Step 4: Python Virtual Machine (PVM)

The Python Virtual Machine is the heart of execution.

What PVM does:

  • Reads bytecode instruction by instruction

  • Executes operations like:

    • variable assignment

    • arithmetic

    • function calls

    • loops and conditions

Example internal execution:

LOAD_CONST 10 STORE_NAME x LOAD_CONST 20 STORE_NAME y LOAD_NAME x LOAD_NAME y BINARY_ADD PRINT

📌 This is why Python is called interpreted
the bytecode is interpreted at runtime.


🧩 Step 5: Library Modules

From your image, Library Modules feed into the Virtual Machine.

Example:

import math print(math.sqrt(25))

What happens internally:

  • Python loads compiled bytecode of math module

  • Links it into the running program

  • Executes it via PVM

📌 Even standard libraries follow the same bytecode + VM execution model.


🧩 Step 6: Running Code (Final Output)

The PVM executes bytecode and interacts with:

  • CPU

  • Memory

  • OS system calls

Output:

30

🎉 Program completed!


🧠 Why Python Feels Interpreted

Even though Python compiles first, developers experience it as interpreted because:

✔ No visible compile step
✔ Executes line-by-line behavior
✔ Errors appear at runtime
✔ Dynamic typing resolved during execution


🆚 Python vs Java (Quick Context)

AspectPythonJava
CompilationAutomaticExplicit (javac)
BytecodeYesYes
Virtual MachinePVMJVM
JIT Compilation❌ No✅ Yes
ExecutionInterpretedHybrid (Interp + JIT)

🎯 Interview-Ready Explanation

Python source code is first compiled into bytecode by the Python compiler.
This bytecode is then executed by the Python Virtual Machine, which interprets each instruction at runtime.
Libraries are also loaded as bytecode and executed within the same VM.


📝 Final One-Line Summary

Python = Compiled to Bytecode + Interpreted by Virtual Machine


Optimistic vs Pessimistic Locking in Spring (with Multi-Instance Microservices)

 

Optimistic vs Pessimistic Locking in Spring (with Multi-Instance Microservices)

When multiple users (or services) try to update the same data at the same time, we can easily end up with:

  • Lost updates

  • Inconsistent reads

  • Weird race conditions

To handle this, databases and ORMs provide locking mechanisms. In Spring/JPA, the two most common strategies are:

  • Pessimistic Locking – “Block others while I’m working”

  • Optimistic Locking – “Let others try, I’ll detect conflicts and retry”

In this post, we’ll cover:

  1. The problem: concurrent updates

  2. Pessimistic locking in Spring (with examples)

  3. Optimistic locking in Spring (with examples)

  4. How both behave when you have multiple instances of your Spring Boot service

  5. When to choose which


1. The Problem: Concurrent Updates

Imagine a Product stock table:

idnamestock
1iPhone10

Two requests come in at the same time to buy 3 units each:

  • Request A: reads stock = 10 → decides new stock = 7

  • Request B: reads stock = 10 → decides new stock = 7

If both update without any concurrency control, final stock will be 7 instead of 4 → ❌ lost update.

We need a way to coordinate writes.


2. Pessimistic Locking in Spring

💡 Idea

“Lock now, work safely, then release.”

With pessimistic locking:

  • When one transaction reads a row for update, it locks that row at the database level.

  • Other transactions that try to lock the same row must wait (or fail with a timeout), until the first transaction commits or rolls back.

Under the hood, the DB does something like:

SELECT * FROM product WHERE id = 1 FOR UPDATE;

This row is locked until the transaction ends.


2.1. Entity Example

import jakarta.persistence.*; @Entity public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private int stock; // getters and setters }

2.2. Repository with Pessimistic Lock

import org.springframework.data.jpa.repository.*; import org.springframework.data.jpa.repository.Lock; import jakarta.persistence.LockModeType; import java.util.Optional; public interface ProductRepository extends JpaRepository<Product, Long> { @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("SELECT p FROM Product p WHERE p.id = :id") Optional<Product> findByIdForUpdate(Long id); }
  • @Lock(LockModeType.PESSIMISTIC_WRITE) tells JPA/Hibernate to use a write lock.

  • For supported databases (MySQL, Postgres, etc.), Hibernate will generate SELECT ... FOR UPDATE.


2.3. Service: Decrease Stock with Pessimistic Lock

import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class InventoryService { private final ProductRepository productRepository; public InventoryService(ProductRepository productRepository) { this.productRepository = productRepository; } @Transactional public void purchase(Long productId, int quantity) { // This will lock the row in DB Product product = productRepository.findByIdForUpdate(productId) .orElseThrow(() -> new RuntimeException("Product not found")); if (product.getStock() < quantity) { throw new RuntimeException("Not enough stock"); } product.setStock(product.getStock() - quantity); // No need to explicitly save if entity is managed; // JPA will flush on transaction commit. } }

What happens at runtime?

  1. Transaction starts (@Transactional).

  2. findByIdForUpdate runs → DB locks that row.

  3. If another request tries to lock the same row:

    • It waits until the first transaction finishes

    • Or fails with a lock timeout if configured so.

  4. First transaction updates and commits → lock is released.

  5. Next transaction acquires the lock, reads latest value, proceeds.

This ensures no lost updates, but at the cost of:

  • Blocking

  • Possible deadlocks if you lock multiple rows in different orders


2.4. Pessimistic Locking with Multiple Instances

You might wonder: What if I run 5 instances of my Spring Boot microservice behind a load balancer?

Good news: pessimistic locking still works, because the locking is done at the database level, not in the Java process.

Sequence:

  1. Instance A (Pod 1) starts a transaction, queries SELECT ... FOR UPDATE.

  2. DB locks the row.

  3. Instance B (Pod 2) tries same query → DB makes it wait.

  4. When Instance A commits, DB releases the lock.

  5. Instance B continues, reading the updated row.

So even with multiple instances:

  • All of them talk to the same database

  • The database is the single source of truth

  • Row locks are shared across connections/instances

Important:
To avoid long locks:

  • Keep transactions short

  • Don’t call external services inside a long-running transaction

  • Consider lock timeout settings (DB and JPA properties)


3. Optimistic Locking in Spring

💡 Idea

“Assume no conflict; detect if it happens and retry.”

Optimistic locking is based on a version field:

  • When you read an entity, you read its version.

  • When you update, Hibernate generates SQL like:

UPDATE product SET stock = ?, version = version + 1 WHERE id = ? AND version = ?;

If another transaction updated the same row meanwhile, the version changed → update affects 0 rows → Hibernate throws OptimisticLockException.


3.1. Entity with @Version

import jakarta.persistence.*; @Entity public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private int stock; @Version private Long version; // can be Long, Integer, etc. // getters and setters }
  • @Version is the key here.

  • Hibernate will automatically:

    • Populate it when first persisted

    • Increment it on each update

    • Use it in WHERE clause on update


3.2. Simple Update with Optimistic Locking

import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class OptimisticInventoryService { private final ProductRepository productRepository; public OptimisticInventoryService(ProductRepository productRepository) { this.productRepository = productRepository; } @Transactional public void purchase(Long productId, int quantity) { Product product = productRepository.findById(productId) .orElseThrow(() -> new RuntimeException("Product not found")); if (product.getStock() < quantity) { throw new RuntimeException("Not enough stock"); } product.setStock(product.getStock() - quantity); // On commit, Hibernate will check version. } }

Scenario with concurrent updates:

  • Initial state: stock=10, version=1

🔹 Transaction A:

  • Reads product → (stock=10, version=1)

  • Decreases stock → 7

  • On commit:
    UPDATE ... SET stock=7, version=2 WHERE id=1 AND version=1;
    → Succeeds → row changes to (stock=7, version=2)

🔹 Transaction B (started at same time):

  • Reads product → (stock=10, version=1)

  • Decreases stock → 7

  • On commit:
    UPDATE ... SET stock=7, version=2 WHERE id=1 AND version=1;
    → Fails (because version is now 2, not 1)
    → Hibernate throws OptimisticLockException / ObjectOptimisticLockingFailureException

So instead of silently overwriting, you get an exception.


3.3. Handling OptimisticLockException (Retry Pattern)

You usually wrap the logic with retry:

import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.transaction.annotation.Transactional; import org.springframework.stereotype.Service; @Service public class RetryingInventoryService { private final ProductRepository productRepository; public RetryingInventoryService(ProductRepository productRepository) { this.productRepository = productRepository; } public void purchaseWithRetry(Long productId, int quantity) { int maxRetries = 3; int attempt = 0; while (true) { try { attempt++; doPurchase(productId, quantity); return; // success, exit loop } catch (ObjectOptimisticLockingFailureException e) { if (attempt >= maxRetries) { throw new RuntimeException("Could not complete purchase after retries", e); } // Optionally add small sleep/backoff here } } } @Transactional protected void doPurchase(Long productId, int quantity) { Product product = productRepository.findById(productId) .orElseThrow(() -> new RuntimeException("Product not found")); if (product.getStock() < quantity) { throw new RuntimeException("Not enough stock"); } product.setStock(product.getStock() - quantity); } }

This is the common optimistic locking pattern:

  1. Try to update

  2. If conflict → catch exception

  3. Reload latest data and retry (up to N times)


3.4. Optimistic Locking with Multiple Instances

Again, the concurrency control happens at the database level via the version column.

Instances don’t need to know about each other:

  • Instance A and B each read from DB and see the version.

  • On update, DB enforces that only one can successfully commit with that version.

  • The other instance gets an exception.

So optimistic locking:

  • Works fine in multi-instance microservice deployments

  • Is scalable, because you don’t block rows

  • Is great for read-heavy, low-contention scenarios


4. Pessimistic vs Optimistic Locking — When to Use What?

🧪 Quick Comparison

AspectPessimistic LockingOptimistic Locking
StrategyLock row immediatelyAllow concurrency, detect conflicts at commit
DB behaviorSELECT ... FOR UPDATE row locksUPDATE ... WHERE version = ? check
Performance under high contentionCan cause blocking, deadlocksMany retries / failures but no blocking
Good forHigh-conflict updates, critical writesRead-heavy workloads, rare conflicts
Multi-instance supportYes (DB-level lock)Yes (DB-level version check)
Failure modeLock timeout / deadlockOptimisticLockException

General guidelines:

  • Use pessimistic locking when:

    • Conflicts are very frequent

    • You’d rather block than retry

    • Operation is critical and you want strict serialization (e.g., bank transfers, inventory with tiny stock)

  • Use optimistic locking when:

    • Most of the time, conflicts don’t happen

    • You need high throughput

    • Occasional retries are acceptable


5. Spring Transaction & Locking Tips (Interview Friendly)

  • Locking works inside a transaction, so @Transactional is important.

  • Default isolation in many databases with Spring is READ_COMMITTED.

  • Pessimistic locking relies on DB support for lock modes (e.g., MySQL InnoDB, Postgres).

  • Multiple instances do not break these strategies, because:

    • They share the same DB

    • Locks/versions are enforced in the DB, not in JVM memory.


6. Summary

  • Pessimistic locking:

    “I will lock the row now so nobody else can change it while I’m working.”
    ✅ Safe, but can block and cause deadlocks.

  • Optimistic locking:

    “I assume no one else will change it; if they do, I’ll detect it and retry.”
    ✅ High throughput, but needs retry logic.

In distributed, multi-instance Spring Boot microservices:

  • Both work, as long as you rely on database-level concurrency control.

  • The database is your global lock manager.

System Design + Java Program for Tic-Tac-Toe (TicTac)

 

🎮 System Design + Java Program for Tic-Tac-Toe (TicTac)

In this mini system design, we’ll build a console-based Tic-Tac-Toe game in Java.

We’ll keep it:

  • Simple

  • Readable

  • Data-structure focused


🧠 High-Level Design

We’ll design 3 main parts:

  1. Board – stores the grid and game state

  2. Player – represents X and O

  3. Game – controls turns, input, and winning logic

Data Structures Used

  • char[][] board = new char[3][3];
    → 3x3 grid for the game

  • char currentPlayer = 'X';
    → Tracks who is playing now

  • Simple methods to:

    • printBoard()

    • makeMove(row, col)

    • checkWin()

    • isDraw()

No complex frameworks. Just core Java + basic data structures.


📦 Class Design Overview

1️⃣ Board Class

Responsibilities:

  • Initialize empty board

  • Print the board

  • Validate and update moves

  • Check winner / draw

2️⃣ TicTacToeGame Class

Responsibilities:

  • Run the game loop

  • Switch players

  • Take input from console

  • Use Board to perform actions


✅ Full Java Program – Simple Tic-Tac-Toe

import java.util.Scanner; /** * Simple Tic-Tac-Toe game using basic OOP and array data structure. * * Design: * - Board: handles the grid and game state * - TicTacToeGame: controls the game flow */ class Board { private char[][] grid; // 3x3 board public Board() { grid = new char[3][3]; // Initialize board with spaces for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { grid[i][j] = ' '; } } } // Print board in a simple format public void printBoard() { System.out.println("-------------"); for (int i = 0; i < 3; i++) { System.out.print("| "); for (int j = 0; j < 3; j++) { System.out.print(grid[i][j] + " | "); } System.out.println(); System.out.println("-------------"); } } // Try to place a move on the board public boolean placeMove(int row, int col, char playerSymbol) { // Check if row and col are in range and cell is empty if (row < 0 || row >= 3 || col < 0 || col >= 3) { System.out.println("Invalid position! Choose row and col between 0 and 2."); return false; } if (grid[row][col] != ' ') { System.out.println("Cell already taken! Choose another position."); return false; } grid[row][col] = playerSymbol; return true; } public boolean hasWinner(char playerSymbol) { // Check rows for (int i = 0; i < 3; i++) { if (grid[i][0] == playerSymbol && grid[i][1] == playerSymbol && grid[i][2] == playerSymbol) { return true; } } // Check columns for (int j = 0; j < 3; j++) { if (grid[0][j] == playerSymbol && grid[1][j] == playerSymbol && grid[2][j] == playerSymbol) { return true; } } // Check diagonals if (grid[0][0] == playerSymbol && grid[1][1] == playerSymbol && grid[2][2] == playerSymbol) { return true; } if (grid[0][2] == playerSymbol && grid[1][1] == playerSymbol && grid[2][0] == playerSymbol) { return true; } return false; } public boolean isFull() { // If any cell is an empty space, board is not full for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if (grid[i][j] == ' ') { return false; } } } return true; } } public class TicTacToeGame { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); Board board = new Board(); char currentPlayer = 'X'; boolean gameFinished = false; System.out.println("Welcome to Tic-Tac-Toe!"); System.out.println("Player X and Player O take turns."); System.out.println("Enter row and column (0, 1, or 2)."); System.out.println(); while (!gameFinished) { board.printBoard(); System.out.println("Player " + currentPlayer + ", it's your turn."); System.out.print("Enter row (0-2): "); int row = scanner.nextInt(); System.out.print("Enter col (0-2): "); int col = scanner.nextInt(); boolean moveSuccess = board.placeMove(row, col, currentPlayer); if (!moveSuccess) { // Invalid move, ask again continue; } // Check win if (board.hasWinner(currentPlayer)) { board.printBoard(); System.out.println("Player " + currentPlayer + " wins! 🎉"); gameFinished = true; } // Check draw else if (board.isFull()) { board.printBoard(); System.out.println("It's a draw! 🤝"); gameFinished = true; } else { // Switch player currentPlayer = (currentPlayer == 'X') ? 'O' : 'X'; } } scanner.close(); System.out.println("Game over. Thanks for playing!"); } }

🧩 How This Relates to System Design (in a Simple Way)

Even though this is a small console program, it still uses good design ideas:

  • Separation of Concerns

    • Board → game state & logic

    • TicTacToeGame → input + game loop

  • Encapsulation

    • Board hides its internal grid and exposes only methods

  • Data Structures

    • 2D array char[][] is the core structure

Ray – Simple Explanation for Interviews (with Architecture & Spark Comparison)

 

Ray – Simple Explanation for Interviews (with Architecture & Spark Comparison)

1️⃣ What is Ray?

Ray is a distributed computing framework used to:

  • Run Python workloads in parallel

  • Scale from laptop → cluster

  • Support AI, ML, data processing, and agents

In simple words

Ray helps you run Python code in parallel across multiple CPUs, GPUs, and machines.


2️⃣ Why Ray was created?

Traditional systems had gaps:

  • Threads / multiprocessing → hard to scale beyond one machine

  • Spark → great for batch data, not flexible for ML & AI

  • Kubernetes → infrastructure, not programming model

👉 Ray fills the gap between Python simplicity and distributed scale.


3️⃣ Simple Ray Example

Without Ray (single CPU)

def work(x): return x * x results = [work(i) for i in range(10)]

With Ray (parallel & distributed)

import ray ray.init() @ray.remote def work(x): return x * x results = ray.get([work.remote(i) for i in range(10)])

✔ Same logic
✔ Runs in parallel
✔ Can scale across machines


4️⃣ Core Ray Concepts (VERY IMPORTANT)

🔹 Tasks

  • Stateless functions

  • Run in parallel

@ray.remote def task(): pass

🔹 Actors

  • Stateful workers

  • Maintain internal state

@ray.remote class Counter: def __init__(self): self.count = 0

🔹 Objects

  • Data stored in distributed shared memory

  • Zero-copy where possible

obj_ref = ray.put(data)

5️⃣ Ray Architecture (Simple View)

┌───────────────────────────┐ │ Ray Head Node │ │---------------------------│ │ Global Control Store │ │ Scheduler │ │ Metadata / Cluster Mgmt │ └───────────┬───────────────┘ │ ┌────────────────┴────────────────┐ │ │ ┌──────────────────────┐ ┌──────────────────────┐ │ Worker Node 1 │ │ Worker Node 2 │ │----------------------│ │----------------------│ │ Ray Workers │ │ Ray Workers │ │ CPU / GPU │ │ CPU / GPU │ │ Object Store │ │ Object Store │ └──────────────────────┘ └──────────────────────┘

6️⃣ How Ray Works (Step-by-Step)

  1. Driver program starts (ray.init())

  2. Ray connects to head node

  3. Tasks / actors are submitted

  4. Scheduler decides where to run them

  5. Data stored in object store

  6. Results returned as object references

👉 You never manage threads or machines directly.


7️⃣ Ray Use Cases (Real-World)

AI / ML

  • Distributed model training

  • Hyperparameter tuning

  • Reinforcement learning

LLM & Agent Systems

  • Multi-agent execution

  • Tool calling

  • Parallel reasoning

Data Processing

  • Parallel ETL

  • Feature engineering

Interview line

Ray is widely used for scalable AI, ML, and agent-based systems.


8️⃣ Ray vs Spark (VERY COMMON INTERVIEW QUESTION)

High-Level Comparison

FeatureRaySpark
LanguagePython-firstScala / Java / Python
Execution ModelTask & ActorBatch / DAG
LatencyLowHigher
ML / AIExcellentLimited
StreamingNot primaryStrong
FlexibilityVery highStructured
Use CaseAI, agents, MLBig data analytics

Conceptual Difference

Spark

  • Data-centric

  • Batch-oriented

  • Optimized for ETL & analytics

Ray

  • Compute-centric

  • Task-oriented

  • Optimized for parallel Python & AI


Simple analogy

  • Spark → Big factory processing large data batches

  • Ray → Smart coordinator running many small jobs in parallel


9️⃣ When to use Ray?

Use Ray when:

  • You have Python workloads

  • Need low-latency parallelism

  • Working on ML, AI, LLMs, agents

  • Need flexibility

Use Spark when:

  • Heavy data analytics

  • SQL-like processing

  • Large batch ETL jobs


🔟 Ray + Spark together?

Yes ✅

Common pattern:

  • Spark → Big data processing

  • Ray → ML training on processed data


1️⃣1️⃣ Interview One-Liners (MEMORIZE)

  • What is Ray?

    Ray is a distributed execution framework for parallel Python workloads.

  • How Ray works?

    Ray schedules tasks and actors across a cluster using a shared object store.

  • Ray vs Spark?

    Spark is data-centric and batch-oriented, while Ray is compute-centric and flexible for AI workloads.

  • Why Ray for AI?

    Ray supports low-latency task execution, actors, and GPU scheduling, making it ideal for AI systems.


1️⃣2️⃣ Final Summary

Ray is a flexible, Python-first distributed computing framework designed for scalable AI, ML, and parallel workloads, offering lower latency and more control than traditional data processing engines like Spark.

Confusion Matrix + Precision/Recall (Super Simple, With Examples)

  Confusion Matrix + Precision/Recall (Super Simple, With Examples) 1) Binary Classification Setup Binary classification means the model p...

Featured Posts