Home Procedural Programming
Post
Cancel
Procedural Programming | SEG

Procedural Programming

This post builds on Imperative Programming. You need basic Java variables, arrays, loops, and method calls. We will use those tools to organize a program into procedures; the complete example runs on Java 17 or later.

What Is Procedural Programming?

Procedural programming organizes a program around named routines that perform parts of a task. Each routine groups related instructions behind a callable name. A larger operation coordinates these routines, passing data between them and controlling the order in which work happens.

Suppose a training application reports how many participants passed an assessment. One block of code could inspect every score, count the passes, construct a message, and print it. Procedural decomposition gives those responsibilities names: count the passing scores, format the summary, then display it. You can understand the overall workflow without reading every instruction in the counting loop.

This builds on the imperative style. Imperative code describes steps and changes to state; procedural code structures those steps into routines. A program can therefore be both imperative and procedural. Moving arbitrary chunks into methods is not enough: useful procedures represent coherent operations with understandable inputs, results, and effects.

Languages use terms such as procedure, function, subroutine, and method with different conventions. Here, a procedure means a named routine, whether it returns a value or performs an action. Java expresses these routines as methods. A method returning int supplies a result; a void method returns no value. The Java method documentation explains these declarations.

Give an Operation a Clear Boundary

A procedure’s boundary tells the caller what information to supply and what to expect in return. Start with a small rule: a score passes when it reaches a cutoff.

1
2
3
static boolean isPassing(int score, int cutoff) {
    return score >= cutoff;
}

In this declaration, score and cutoff are parameters: names the method uses for its inputs. In the call isPassing(72, 60), 72 and 60 are arguments: the supplied values. The method returns true, which the caller can use in a decision.

The return type boolean makes the result explicit. static allows us to call this method without creating an instance of the surrounding class. Java requires this method to live inside a class, but this example organizes behavior around operations on supplied data rather than around objects that own assessment state.

Notice that the cutoff is an input. If the method instead read a mutable class field, understanding isPassing(72) would also require knowing the current value of that field. Explicit parameters make the dependency visible and let the same operation serve different assessments.

The name expresses a meaningful question. Extracting score >= cutoff is useful if that rule deserves a shared name or will evolve independently. For our small report, we will keep the comparison inside the counting procedure so the reader does not need to jump between methods to understand one simple rule.

Decompose a Workflow by Responsibility

Here is a first version of the report. This snippet belongs inside main:

1
2
3
4
5
6
7
8
9
10
11
12
int[] scores = {48, 72, 90, 60};
int cutoff = 60;
int passed = 0;

for (int score : scores) {
    if (score >= cutoff) {
        passed++;
    }
}

String summary = "Passed: " + passed + " of " + scores.length;
System.out.println(summary);

The enhanced for loop visits each score. The if statement selects the scores that qualify, and passed++ adds one to the running count. After the loop, the code builds and prints Passed: 3 of 4.

The code is short, but it combines three responsibilities. Counting defines the assessment rule, formatting decides how the result is worded, and printing interacts with the outside world. These responsibilities give us natural procedure boundaries.

First, extract the counting operation:

1
2
3
4
5
6
7
8
9
static int countPassing(int[] scores, int cutoff) {
    int count = 0;
    for (int score : scores) {
        if (score >= cutoff) {
            count++;
        }
    }
    return count;
}

The loop still performs explicit state changes. What changes is their location: the caller asks for a count without managing the loop’s accumulator. The procedure’s contract is small: given a non-null array of scores and a cutoff, return how many scores are at least that cutoff. It does not print or alter the scores.

Then name the formatting operation:

1
2
3
static String formatSummary(int passed, int total) {
    return "Passed: " + passed + " of " + total;
}

This method receives the two facts needed to produce the message. It does not need the whole array or the cutoff. Passing only the necessary information keeps the boundary narrow: changing the wording cannot accidentally change the assessment data through this method’s parameters.

Local State Belongs to One Call

The count variable is local state. Each call to countPassing starts with its own count initialized to zero. After the method returns, callers work with the returned number; they cannot access that local variable.

If we moved the accumulator to a mutable static field, a second call could accidentally retain the first call’s count. We would also introduce coordination problems when calls overlap. Keeping temporary state local removes that dependency between calls.

Local variables do not make every operation isolated. In Java, argument values are copied into parameters, including reference values. The scores parameter refers to the same array supplied by the caller. An assignment such as scores[0] = 0 inside the method would change that shared array, even though the parameter itself is local. Reassigning the parameter to another array would not reassign the caller’s variable. See Java’s explanation of passing arguments.

Our method only reads the array. Its local updates to count therefore do not change the caller’s input.

A Complete Java Example

Save this as AssessmentReport.java. The inputs are deliberately small: a non-null array containing scores from 0 to 100 and a cutoff in the same range. A larger application would validate external input before running the report.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class AssessmentReport {
    static int countPassing(int[] scores, int cutoff) {
        int count = 0;
        for (int score : scores) {
            if (score >= cutoff) {
                count++;
            }
        }
        return count;
    }

    static String formatSummary(int passed, int total) {
        return "Passed: " + passed + " of " + total;
    }

    public static void main(String[] args) {
        int[] scores = {48, 72, 90, 60};
        int cutoff = 60;

        // Coordinate the steps and pass each result to its next consumer.
        int passed = countPassing(scores, cutoff);
        String summary = formatSummary(passed, scores.length);

        // Console output is an explicit side effect of the workflow.
        System.out.println(summary);
    }
}

Compile and run it:

1
2
javac --release 17 AssessmentReport.java
java AssessmentReport

The output is:

1
Passed: 3 of 4

Follow the Call and Return Order

Execution begins in main, which prepares the scores and cutoff. When main calls countPassing, it waits for that ordinary synchronous call to finish before assigning the result to passed.

Inside countPassing, the accumulator evolves like this:

Score inspectedReaches cutoff 60?Count after this iteration
48No0
72Yes1
90Yes2
60Yes3

The return statement sends 3 back to the caller and ends that method call. main then calls formatSummary(3, 4), receives the string Passed: 3 of 4, and passes it to println. The output occurs after the calculation and formatting.

This illustrates sequence between calls, iteration through the array, and selection within the loop. These are the control structures that make the execution order explicit. Java’s control flow overview describes how decisions, loops, and branches affect execution.

The physical order of method declarations does not determine their execution order. countPassing appears above main in the file, but it runs because main calls it. A method may also call another method, building a hierarchy of operations. Our example needs only one coordinating level.

Try a cutoff of 80: the same counting procedure returns 1, and the summary becomes Passed: 1 of 4. An empty array returns 0 and produces Passed: 0 of 0. Both cases use a fresh accumulator without resetting any shared variable.

Make Side Effects Visible

A side effect is an observable interaction beyond returning a result, such as printing, changing a supplied object, or writing a file. Procedural programs commonly perform effects; organizing them into clear steps makes their timing easier to understand.

In our report, countPassing calculates a number and formatSummary calculates a string. println performs the output. You can reuse the count in another calculation or send the summary to a web response without producing console output along the way.

If countPassing printed each passing score, calling it twice would print twice. If it updated a shared total, later calls could depend on earlier calls. A returned int alone would no longer describe everything the method does. Effects belong in the contract too.

Name procedures so their responsibilities are clear, and document effects that callers need to know about. A method called countPassing should not quietly save an assessment or send a notification.

Separate procedures do not automatically give you a transaction. In a workflow that writes a file and then sends a notification, the notification may fail after the file was written. The coordinator still needs an explicit failure policy. Procedure boundaries help identify the steps, but do not undo completed effects.

How It Relates to Other Styles

The same assessment problem can be organized in several ways:

StyleMain organizing ideaAssessment example
ImperativeInstructions update state in an explicit orderLoop over scores and increment a counter
ProceduralNamed routines divide and coordinate that workCall counting and formatting routines, then display the result
Object-orientedObjects own state and expose behaviorAn assessment object protects its scores and offers operations on them
FunctionalFunctions transform values with controlled dependencies and effectsDerive a count from stable score values and keep output separate

These ideas overlap. Our counting routine uses a mutable local accumulator, but it also behaves as a pure calculation when its input array remains stable during the call: the same input values yield the same result without observable effects. The Functional Programming article develops purity, immutability, and function composition further.

In an object-oriented design, an assessment object could own the scores and ensure they always satisfy its rules. Its methods could still use procedural decomposition internally. Merely wrapping static methods in a Java class does not provide that ownership model; our class serves as a container for routines.

Choose Procedures That Explain the Work

Procedural decomposition fits command-line tools, batch processing, numerical algorithms, and application workflows where the sequence of operations matters. It also helps inside larger object-oriented systems: a service method can coordinate clear operations without making all the details visible at once.

Good boundaries follow responsibilities that can be explained and changed independently. In this example, counting and wording have separate reasons to change. A new passing rule affects the calculation, while a new report phrase affects formatting. Neither requires moving console output into the calculation.

There are limits. A large collection of procedures sharing mutable global data becomes difficult to reason about. When many routines need the same related values and must preserve rules between them, encapsulating that state in an object may provide a stronger boundary. When calculations are mainly transformations of data, functional techniques may express them more directly.

Avoid extracting every line just to shorten methods. Too many tiny wrappers scatter the explanation across the file. A useful procedure name should let its caller understand an operation without opening the body for every call.

For practice, change the report to include a failing count without changing countPassing. Decide whether that count is best derived from the returned value or calculated by another loop, and explain why. Then identify each procedure’s inputs, result, local state, and effects. Those boundaries are the foundation of a procedural design you can understand and maintain.

This post is licensed under CC BY 4.0 by the author.