☕ Java

Stored Procedures

Stored procedures are precompiled routines that live and execute inside the database itself, written in a vendor-specific procedural SQL dialect (PL/pgSQL for PostgreSQL, PL/SQL for Oracle, T-SQL for SQL Server, procedural SQL for MySQL/MariaDB) rather than in Java, and JDBC invokes them through CallableStatement using the standardized call escape syntax. Choosing to push logic into the database as a stored procedure versus keeping it in application code is a real architectural decision with tradeoffs in performance, portability, testability, and team workflow, not just a syntax choice. This entry covers what stored procedures are and why teams use them, the JDBC invocation mechanics in context, and the practical tradeoffs against keeping equivalent logic in application code.

What Stored Procedures Are and Why They Exist

A stored procedure is a named, precompiled block of procedural logic stored inside the database server itself and invoked by name (optionally with arguments) rather than by sending its full logic as SQL text on every call. Unlike a single SQL statement, a stored procedure's body can contain control flow (loops, conditionals), multiple SQL statements executed in sequence, exception handling specific to the database's procedural dialect, and — depending on the database — transaction control of its own, all running entirely on the database server without round-tripping back to the calling application between steps. Each major database vendor has its own dialect: Oracle's PL/SQL, PostgreSQL's PL/pgSQL (and other supported procedural languages), Microsoft SQL Server's Transact-SQL, and MySQL/MariaDB's own procedural SQL extension — these dialects are not interchangeable, and a procedure written for one database generally needs to be substantially rewritten, not just lightly adapted, to run on another. Teams reach for stored procedures for several recurring reasons: keeping logic that operates heavily on data physically close to that data, avoiding the network round trips that an equivalent multi-step operation expressed entirely in application code would require; centralizing a piece of business logic so that every application or service touching the database — not just the one written in Java — automatically gets the same behavior, including ad hoc access via a database client; and, in some organizations, separating database-administration responsibilities (where DBAs manage and tune stored procedures directly) from application-development responsibilities. None of these motivations are universal requirements, and many teams build equally correct systems with no stored procedures at all, keeping all such logic in application code instead — covered further in the tradeoffs section below.
sql
-- Illustrative PostgreSQL (PL/pgSQL) stored procedure: applies a discount
-- and logs the change, all in one server-side round trip
CREATE OR REPLACE PROCEDURE apply_discount(p_product_id BIGINT, p_percent NUMERIC)
LANGUAGE plpgsql
AS $$
BEGIN
    UPDATE products
    SET price = price * (1 - p_percent)
    WHERE id = p_product_id;

    INSERT INTO price_change_log (product_id, change_type, changed_at)
    VALUES (p_product_id, 'discount', NOW());
END;
$$;

-- Illustrative stored function (returns a value, unlike a procedure):
CREATE OR REPLACE FUNCTION calculate_tax(p_amount NUMERIC)
RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
BEGIN
    RETURN p_amount * 0.08;
END;
$$;

/* The equivalent logic expressed entirely as separate statements from the
   application side would require two distinct round trips (UPDATE, then
   INSERT) rather than one call into a single server-side procedure. */

Invoking Stored Procedures from JDBC — Recap in Context

From the JDBC side, invoking a stored procedure or function always goes through CallableStatement, created via conn.prepareCall() with the standardized {call procedure_name(?, ?)} or {? = call function_name(?)} escape syntax — this is the same mechanism covered in depth in the dedicated CallableStatement entry, and is repeated here specifically in the context of deciding to use stored procedures at all rather than as new mechanics. Parameters are categorized as IN (supplied via the ordinary setXxx() setters), OUT (the procedure's way of returning a value through a parameter rather than a function return value, requiring registerOutParameter() before execution and a getXxx() call afterward), or INOUT (both), and a procedure that internally runs more than one query can surface multiple result sets to the caller, retrieved via getResultSet() and getMoreResults() in sequence. What matters architecturally is that, from JDBC's point of view, a stored procedure call is a single network round trip regardless of how much work the procedure does internally — this is precisely the performance argument in favor of stored procedures for operations that would otherwise require several round trips if expressed as separate statements from application code, and precisely why the choice to push something into a stored procedure is, in part, a decision about where round-trip cost should be paid: once, inside the database, or potentially multiple times, between the application and the database.
Java
import java.sql.*;

public class InvokingStoredProcedureRecap {
    public static void applyDiscount(Connection conn, long productId, java.math.BigDecimal percent)
            throws SQLException {
        // Single round trip — the UPDATE and the audit-log INSERT both happen
        // inside the database as part of this one call
        try (CallableStatement cs = conn.prepareCall("{call apply_discount(?, ?)}")) {
            cs.setLong(1, productId);
            cs.setBigDecimal(2, percent);
            cs.execute();
        }
    }

    public static java.math.BigDecimal calculateTax(Connection conn, java.math.BigDecimal amount)
            throws SQLException {
        try (CallableStatement cs = conn.prepareCall("{? = call calculate_tax(?)}")) {
            cs.registerOutParameter(1, Types.NUMERIC);
            cs.setBigDecimal(2, amount);
            cs.execute();
            return cs.getBigDecimal(1);
        }
    }

    // ── Contrast: the same discount logic expressed as application-side
    // statements instead — two round trips rather than one ─────────────────
    public static void applyDiscountInApplicationCode(Connection conn, long productId, java.math.BigDecimal percent)
            throws SQLException {
        try (PreparedStatement updatePrice = conn.prepareStatement(
                "UPDATE products SET price = price * (1 - ?) WHERE id = ?")) {
            updatePrice.setBigDecimal(1, percent);
            updatePrice.setLong(2, productId);
            updatePrice.executeUpdate();   // round trip 1
        }
        try (PreparedStatement logChange = conn.prepareStatement(
                "INSERT INTO price_change_log (product_id, change_type, changed_at) VALUES (?, 'discount', NOW())")) {
            logChange.setLong(1, productId);
            logChange.executeUpdate();     // round trip 2
        }
    }
}

Tradeoffs — Stored Procedures vs. Application-Level Logic

The performance argument for stored procedures — fewer network round trips for multi-step operations — is real but often overstated relative to its actual impact in a typical application: for an application server and database that are co-located on the same network with low latency, and for operations that aren't executed at extremely high frequency, the round-trip savings from moving logic into a stored procedure are frequently small relative to the cost of writing, testing, and especially debugging that logic in a separate language and toolchain from the rest of the application. Where the performance argument is strongest is genuinely high-frequency, multi-step operations, or scenarios with meaningfully higher network latency between application and database than a typical co-located deployment. Against that performance argument sit several real costs: stored procedure code is written in a vendor-specific dialect that most application developers don't work in daily, has comparatively weaker tooling for unit testing, debugging, version control integration, and code review compared to mainstream application languages, and ties the application's behavior to a specific database vendor in a way that complicates ever migrating to a different database — a stored-procedure-heavy codebase effectively duplicates a portion of its business logic in SQL dialect rather than centralizing it in the application's primary language. Many modern teams, especially those building cloud-native or microservice-oriented systems with strong emphasis on automated testing and continuous deployment, deliberately keep business logic in application code and use the database purely for storage and simple constraints, reserving stored procedures for the narrower set of cases (genuinely round-trip-sensitive operations, or organizational requirements imposed by a DBA team) where their specific benefits clearly outweigh these costs — there's no universally correct answer, and the right balance depends on team composition, performance requirements, and how much portability across database vendors actually matters for that specific system.
Java
// There's no single "correct" code example for a tradeoffs discussion — but
// here's a concrete illustration of the kind of decision point teams face:
// a multi-step inventory adjustment that COULD be a stored procedure or
// COULD stay in application code.

// ── Option A: stored procedure — one round trip, vendor-specific dialect ──
// CREATE PROCEDURE adjust_inventory(p_product_id BIGINT, p_delta INT)
// LANGUAGE plpgsql AS $$
// BEGIN
//     UPDATE inventory SET quantity = quantity + p_delta WHERE product_id = p_product_id;
//     IF (SELECT quantity FROM inventory WHERE product_id = p_product_id) < 0 THEN
//         RAISE EXCEPTION 'Insufficient inventory for product %', p_product_id;
//     END IF;
//     INSERT INTO inventory_audit (product_id, delta, adjusted_at) VALUES (p_product_id, p_delta, NOW());
// END; $$;

import java.sql.*;

public class InventoryAdjustmentTradeoff {
    // ── Option B: same logic in application code — portable, testable in Java,
    // costs an extra round trip, and the invariant check happens in app code
    // rather than inside a single atomic database-side block ───────────────
    public static void adjustInventoryInApplicationCode(Connection conn, long productId, int delta)
            throws SQLException {
        conn.setAutoCommit(false);
        try {
            try (PreparedStatement update = conn.prepareStatement(
                    "UPDATE inventory SET quantity = quantity + ? WHERE product_id = ?")) {
                update.setInt(1, delta);
                update.setLong(2, productId);
                update.executeUpdate();
            }

            try (PreparedStatement check = conn.prepareStatement(
                    "SELECT quantity FROM inventory WHERE product_id = ?")) {
                check.setLong(1, productId);
                try (ResultSet rs = check.executeQuery()) {
                    rs.next();
                    if (rs.getInt("quantity") < 0) {
                        throw new SQLException("Insufficient inventory for product " + productId);
                    }
                }
            }

            try (PreparedStatement audit = conn.prepareStatement(
                    "INSERT INTO inventory_audit (product_id, delta, adjusted_at) VALUES (?, ?, NOW())")) {
                audit.setLong(1, productId);
                audit.setInt(2, delta);
                audit.executeUpdate();
            }

            conn.commit();
        } catch (SQLException e) {
            conn.rollback();
            throw e;
        } finally {
            conn.setAutoCommit(true);
        }
    }
}

Related Topics in Java Database Connectivity (JDBC)

JDBC Architecture
JDBC (Java Database Connectivity) is the standard Java API for connecting to and executing operations against relational databases, defined as a set of interfaces in java.sql and javax.sql that database vendors implement with concrete driver classes. Its architecture is deliberately layered to decouple application code from any specific database product: applications code against interfaces like Connection, Statement, and ResultSet, while a pluggable driver underneath translates those calls into the vendor-specific network protocol the database actually understands. This entry covers the two-layer (API/driver) architecture, the four historical driver types, the role of the JDBC driver manager and connection pooling layers, and how the pieces fit together at runtime.
JDBC Drivers
A JDBC driver is the vendor-specific library that implements the java.sql/javax.sql interfaces for a particular database product, translating standard JDBC calls into that database's native network protocol. Choosing, loading, registering, and configuring the correct driver is a prerequisite for any JDBC code to run, and the mechanics of how a driver gets discovered and instantiated have evolved significantly since JDBC's introduction. This entry covers driver registration via the Service Provider Interface (SPI) versus the older explicit Class.forName() approach, JDBC URL syntax and structure, driver-specific configuration properties, and how to choose and manage driver dependencies in a real project.
DriverManager
java.sql.DriverManager is the original, classic entry point into JDBC: a static factory class that maintains a registry of available Driver implementations and hands back a live Connection when given a JDBC URL and credentials. It predates the javax.sql.DataSource abstraction and connection pooling, and while it remains fully functional and is still commonly used for simple applications, scripts, and tests, production server-side code generally prefers DataSource-based connection pooling for the reasons covered below. This entry covers how DriverManager locates and selects a driver, its core methods, login timeout and logging configuration, and where it fits (and doesn't fit) in modern application architecture.
Connection
java.sql.Connection represents a single session with a specific database — the live link through which all statements, transactions, and metadata queries flow, and the factory from which Statement, PreparedStatement, and CallableStatement objects are created. A Connection is a relatively expensive, stateful resource that wraps an underlying network socket and server-side session, which is why its lifecycle, transaction settings, and proper closing matter so much in practice. This entry covers obtaining and closing connections, transaction control (auto-commit, commit, rollback, savepoints, isolation levels), creating statements from a connection, and connection metadata and health checks.