You've written functions. But you've called methods. Wait — isn't that just a function? Maybe you've even built a class or two. But somewhere along the way, someone dropped the word "procedure" in a code review or a textbook, and you paused. Here's the thing — a subroutine? A fancy name for the same thing?
Short answer: sort of. Long answer: the distinction matters more than most tutorials let on.
What Is a Procedure in Programming
A procedure is a named block of code that performs a specific task. That's the textbook definition. But in practice, it's a reusable chunk of logic you can call by name — usually without expecting a return value.
Here's where it gets muddy. Visual Basic uses Sub. Some return values. In some languages, procedure* is a formal keyword. Also, sQL has stored procedures. Some don't. Ada has procedure. On the flip side, in others — Python, JavaScript, Java, C# — the word doesn't exist as a keyword. Worth adding: pascal has procedure. You write functions or methods. The language doesn't care what you call them.
So why does the term persist?
Because procedure* carries intent. It sends an email.Plus, it signals: "This code does something. In practice, it writes to a database. It prints a report. Now, " It's not about computing a value. Worth adding: it changes state. It's about action*.
Procedure vs Function: The Difference That Isn't Always One
In computer science theory, the distinction is clean:
- A function takes input, computes a result, and returns it. No side effects. Same input → same output. Pure.
- A procedure takes input, performs actions, and returns nothing (or just a status code). Side effects are the point.
Real world? Messier.
In C, a void function is a procedure by behavior. And in Python, a function that returns None implicitly? Worth adding: in Java, a void method — same deal. On the flip side, the language syntax says "function" or "method. Also a procedure. " The role* says procedure.
And stored procedures in databases? Worth adding: they're their own beast. They live on the server. They can run transactions, loop through cursors, call other procedures. They're not just "functions that live in SQL." They're programmable units with their own execution context, permissions, and performance profile.
Parameters: In, Out, and In-Out
Procedures often use parameters differently than pure functions.
- IN parameters — read-only inputs. The procedure consumes them.
- OUT parameters — write-only outputs. The procedure fills them. The caller reads them after.
- IN OUT parameters — both. The procedure reads the initial value, modifies it, and the caller sees the change.
This pattern shows up in PL/SQL, Ada, and older systems languages. It's rare in modern application code — we'd rather return an object or throw an exception — but it's still alive in systems programming and database work.
Why It Matters / Why People Care
You might wonder: if my language doesn't have a procedure keyword, why should I care?
Because thinking in procedures* changes how you design code.
Separation of Concerns, the Honest Way
Functions that return values are easy to test. They mutate state. Still, they hit APIs. But they talk to databases. Because of that, you call them, assert the output, done. Procedures? Harder. They write files.
But here's the thing: something* has to do those things. You can't build a useful app with pure functions alone. Also, at some layer — usually the edges — you need procedures. The trick is keeping them thin* and obvious*.
A procedure that:
- Validates input
- Calls a pure function to compute a result
- Persists the result
- Logs the outcome
- Returns a success flag
is a well-behaved procedure. One that does all that plus* sends an email, updates a cache, and modifies a global config object? That's a maintenance nightmare waiting to happen.
Transaction Boundaries
In database work, procedures define transaction scope. So a stored procedure can wrap multiple statements in a single atomic unit. Commit or rollback happens at the procedure level. Think about it: that's not just convenience — it's correctness. Try doing that cleanly with ad-hoc SQL from application code. You'll either leak connections or end up with partial updates.
Want to learn more? We recommend what three parts make up the nucleotide and what is 40/60 as a percent for further reading.
Performance and Network Round Trips
Call a REST API ten times from your frontend? So ten round trips. Wrap those ten operations in a stored procedure or a backend endpoint that acts like one? Consider this: one round trip. The procedure becomes a batch boundary*. That matters at scale.
Security and Permissions
Procedures (especially stored procedures) can execute with elevated privileges while the caller has minimal rights. The application user doesn't need DELETE on the users table — they just need EXECUTE on deactivate_user. The procedure handles the rest. That's defense in depth, and it's one reason procedures survive in enterprise systems.
How It Works (or How to Write One Well)
Let's walk through what a well-designed procedure looks like in practice. Language-agnostic principles first, then a few concrete patterns.
1. Name It Like a Verb Phrase
process_data() tells you nothing. archive_expired_subscriptions() tells you exactly what it does. Procedures do things. Name them that way.
Bad: handle_user()
Better: register_new_user(), deactivate_user_account(), send_password_reset_email()
2. Keep the Parameter List Short
More than three or four parameters? But you're probably missing an object. Group related data.
Instead of:
def create_order(customer_id, product_id, quantity, shipping_address, billing_address, payment_method, discount_code):
...
Do:
def create_order(order_request: OrderRequest) -> OrderResult:
...
The procedure takes one thing in, returns one thing out (or nothing). Inside, it can destructure all it wants.
3. Validate Early, Fail Fast
A procedure should reject bad input at the door. Don't let invalid data propagate into your business logic or database.
def transfer_funds(from_account: AccountId, to_account: AccountId, amount: Decimal) -> TransferResult:
if amount <= 0:
return TransferResult.failure("Amount must be positive")
if from_account == to_account:
return TransferResult.failure("Cannot transfer to same account")
# ... rest of logic
4. Delegate to Pure Functions
The procedure orchestrates. It doesn't compute.
def generate_monthly_invoice(customer_id: CustomerId) -> Invoice:
customer = repo.get_customer(customer_id)
subscriptions = repo.get_active_subscriptions(customer_id)
line_items = calculate_line_items(subscriptions) # pure function
total = compute_total(line_items) # pure function
invoice = Invoice(customer, line_items, total)
repo.save_invoice(invoice)
email_service.send_invoice(invoice)
return invoice
See? The procedure coordinates*. The math lives in testable, pure functions. The side effects (save, send) stay in the procedure. Clean boundaries.
5. Handle Errors Explicitly
Don't let exceptions bubble up unchecked from a procedure unless you want* the caller to crash. Even so, or a checked exception if your language has them. Use a Result<T, E> type. But return a result object. Make failure a first-class citizen in the signature.
fn process
### 6. Design for Idempotency
In distributed systems, operations may be retried due to timeouts or failures. A well-designed procedure should be idempotent—safe to call multiple times without unintended consequences.
```python
def process_refund(transaction_id: str, amount: Decimal) -> RefundResult:
if refund_already_exists(transaction_id):
return RefundResult.duplicate()
# ... execute refund logic ...
Check for prior execution before acting. Use unique identifiers or state flags to prevent duplicate work.
7. Log Key Steps for Debugging
Procedures should emit logs at critical points: input validation, state changes, external calls. Use structured logging to capture context.
def transfer_funds(from_account: AccountId, to_account: AccountId, amount: Decimal) -> TransferResult:
logger.info("Initiating fund transfer", extra={"from": from_account, "to": to_account, "amount": amount})
# ... logic ...
logger.debug("Transfer completed", extra={"status": "success"})
Logs should help trace failures without exposing sensitive data.