ArticlesProjectsCredentialsAbout
mobileconstraintsj2me

Designing Software for Constrained Devices: RAM, CPU, and Battery

·4 min read

Designing Software for Constrained Devices: RAM, CPU, and Battery

In 1998 the mobile devices we targeted had 64KB of heap memory, a 20MHz processor, and a battery that lasted eight hours if the software was disciplined about power. These were not soft constraints — a heap overflow crashed the device, a busy loop drained the battery in an hour, and a network request that held the CPU awake used power the user could measure.

Building for constrained hardware changed how you thought about software design at every level.

The Memory Constraint

J2ME's CLDC (Connected Limited Device Configuration) gave you between 32KB and 512KB of heap depending on the device. There was no virtual memory, no swap. Allocate too much and you got OutOfMemoryError.

The main tool was object reuse:

// WRONG — allocates a new String on every poll cycle
public String formatStatus(String ip, String status) {
    return ip + ":" + status; // allocates intermediate StringBuilder + result
}

// RIGHT — reuse a StringBuffer
private final StringBuffer buf = new StringBuffer(64);

public String formatStatus(String ip, String status) {
    buf.setLength(0);
    buf.append(ip).append(':').append(status);
    return buf.toString();
}

On a desktop JVM this is unnecessary micro-optimisation. On CLDC it was required to avoid heap exhaustion in long-running applications.

Object pools were used for anything allocated frequently:

public class ByteArrayPool {
    private final Vector pool = new Vector(); // J2ME had no ArrayList
    private final int    bufSize;

    public ByteArrayPool(int bufSize, int initialSize) {
        this.bufSize = bufSize;
        for (int i = 0; i < initialSize; i++) pool.addElement(new byte[bufSize]);
    }

    public synchronized byte[] acquire() {
        if (pool.isEmpty()) return new byte[bufSize];
        byte[] b = (byte[]) pool.lastElement();
        pool.removeElementAt(pool.size() - 1);
        return b;
    }

    public synchronized void release(byte[] b) { pool.addElement(b); }
}

The CPU Constraint

20MHz meant that an operation cheap on a desktop JVM — XML parsing, regular expressions, cryptographic hashing — was expensive enough to make the UI unresponsive.

Rules we followed:

Do not parse on the main thread. Any operation that could take more than 50ms went on a background thread. On J2ME this meant managing threads manually:

public class PollerTask implements Runnable {
    public void run() {
        while (!stopped) {
            pollDevices();
            synchronized (this) {
                try { wait(30000); } catch (InterruptedException e) { break; }
            }
        }
    }
}

Choose simpler algorithms. Bubble sort over quicksort for small collections — the overhead of recursion on a slow JVM sometimes outweighed the algorithmic advantage. Binary search for lookups in sorted arrays rather than hash tables — Hashtable on CLDC had more overhead than a linear scan of a small array.

Avoid string splitting. String operations on CLDC were expensive. We parsed protocol messages by scanning for delimiter bytes in the raw byte array rather than converting to String and calling split.

The Battery Constraint

The radio was the largest power consumer on a mobile device. Every network connection woke the radio, which consumed significant current for several seconds after the connection closed.

Batch network operations. Instead of ten small requests, one larger request. We buffered status updates and sent them in a single payload:

// Collect updates for 30 seconds, send in one request
private final Vector pendingUpdates = new Vector();

public void queueUpdate(StatusUpdate u) {
    pendingUpdates.addElement(u);
}

public void flush() throws IOException {
    if (pendingUpdates.isEmpty()) return;
    byte[] payload = serialise(pendingUpdates);
    sendToServer(payload);
    pendingUpdates.removeAllElements();
}

Respect the polling interval. A device that polled the server every 10 seconds kept the radio alive continuously. Thirty seconds was the minimum interval we used; two minutes was better for battery-sensitive deployments.

Release connections immediately. HTTP keep-alive was disabled on J2ME because holding a connection open held the radio on. Open, request, receive, close.

What Constrained Development Taught

The constraints forced precision. You could not write vague code and hope the JVM would optimise it. You had to understand what every allocation cost, how long every operation took, and what every network call did to the radio.

Those habits transferred directly to performance-sensitive work on unconstrained hardware. A developer who had built for CLDC knew what to profile first, which data structures avoided unnecessary allocation, and why batching was almost always worth the complexity.

Modern hardware makes most of these decisions irrelevant most of the time. Edge computing, IoT, and WebAssembly on embedded targets bring them back. The thinking does not go out of date.