A function opens a file and returns a parser. A factory creates a client backed by a connection pool. A component starts a worker and hands another component a handle. Everything works until shutdown, an exception, or a refactor exposes a basic question that the design never answered: who is responsible for releasing the resource?

Resource leaks are often described as missing cleanup calls. That is only the visible failure. The deeper design problem is ambiguous ownership. If several parts of the program can use a resource but none clearly owns its lifetime, cleanup becomes guesswork.

This article develops a practical mental model for resource ownership: the code that acquires a resource should either release it within the same lifetime or explicitly transfer responsibility to a new owner. With that model, cleanup paths become easier to reason about, test, and change.

Separate access from ownership

Using a resource does not necessarily mean owning it.

Suppose a report generator receives an already-open output stream:

generateReport(order, output)

The generator needs access to output, but the call alone does not tell us whether it should close the stream afterward. The caller may want to write several reports to the same stream. Closing it inside generateReport would then be incorrect.

Ownership answers a different question:

Which component is responsible for deciding when this resource’s lifetime ends and performing the required cleanup?

A useful default is to keep ownership with the code that acquired the resource. If a caller opens the stream, passes it to generateReport, and continues to control the surrounding operation, the caller should normally close it.

caller:
  output = openOutput()
  try:
    generateReport(order, output)
  finally:
    close(output)

This is language-neutral pseudocode. Production code should use the language’s structured resource-management mechanism when one exists, because that mechanism can make cleanup happen reliably across normal returns and exceptional exits.

The important design point is independent of syntax: generateReport borrows the stream; the caller owns it.

Keep acquisition and release in the same scope when possible

The simplest ownership model is lexical: acquire a resource, use it, and release it in one visible scope.

function loadConfig(path):
  file = open(path)
  try:
    return parse(file)
  finally:
    close(file)

Here loadConfig opens the file and finishes all work that requires it before returning. No caller needs to know that parsing involved an open file. The resource lifetime is contained inside the operation.

This design has several useful consequences. The caller cannot accidentally forget to close the file. A later refactor from a file to an in-memory source does not change the caller’s responsibilities. Failure during parsing still passes through the cleanup path.

Containing the lifetime is often preferable when callers need the result produced from a resource rather than the live resource itself.

Compare that with returning an object that still depends on the file:

function openRecords(path):
  file = open(path)
  return RecordIterator(file)

Now the file must remain open after openRecords returns. Acquisition and release cannot both happen inside the function. Ownership must move somewhere else, and the API should make that responsibility clear.

Transfer ownership deliberately

Sometimes a resource must outlive the function that creates it. In that case, treat the return as an ownership transfer rather than an ordinary value return.

Suppose openRecords returns a RecordReader that reads lazily from a file. A coherent contract is:

reader = openRecords(path)
try:
  for record in reader:
    process(record)
finally:
  reader.close()

The factory acquires the underlying file, but the returned reader becomes its owner. The reader’s cleanup operation releases everything it owns.

The API should make this lifetime visible through conventions appropriate to the environment: a close or dispose operation, a structured lifetime construct, clear documentation, or a type that participates in the language’s resource-management protocol.

What matters is that the caller can answer two questions without reading implementation details:

  1. Does this returned object hold resources beyond the call?
  2. What action ends that lifetime?

If those answers are hidden, callers can use the API correctly during normal execution while still leaking resources during repeated use or shutdown.

Ownership should form a clear chain

Real components often own other components, which in turn own resources.

Consider an application that creates a notification service. The service creates a worker pool, and each worker uses connections from a connection pool:

Application
  owns NotificationService
    owns WorkerPool
    owns ConnectionPool

A useful shutdown rule follows the ownership chain in reverse:

Application stops NotificationService
NotificationService stops WorkerPool
NotificationService closes ConnectionPool

The exact order depends on the dependencies between those resources. If workers can still request connections, the workers must stop accepting or performing that work before the pool is closed. Otherwise shutdown can create failures by destroying a dependency while its users are still active.

This is why resource ownership is not merely about preventing leaks. It also defines who coordinates lifecycle transitions.

A component that owns a set of resources should understand enough about their dependency order to release them safely. Code outside that component should not need to reach inside and close individual internals.

Borrowed resources should not be closed by borrowers

Ambiguous ownership causes the opposite of a leak too: premature cleanup.

Imagine a service receives a shared client during construction:

client = createSharedClient()
orders = OrderService(client)
payments = PaymentService(client)

If OrderService.stop() closes client, PaymentService may suddenly fail even though it is still running. OrderService uses the client but does not own it.

The component that created the shared client and decided to share it should normally retain ownership:

application:
  client = createSharedClient()
  orders = OrderService(client)
  payments = PaymentService(client)

  ... run application ...

  stop(orders)
  stop(payments)
  close(client)

This rule prevents a common class of lifecycle bugs: one borrower destroys a dependency that another borrower still needs.

Shared access is compatible with single ownership. Many components may use a resource while one component remains responsible for its lifetime.

Partial construction needs cleanup too

Ownership becomes especially important when acquisition has several steps and a later step can fail.

Suppose startup performs these operations:

pool = createConnectionPool()
workers = startWorkers(pool)
listener = openListener(workers)

If openListener fails, the program has already acquired the pool and started the workers. Returning an error without cleanup leaks both earlier resources.

A robust construction path unwinds what it has successfully acquired:

pool = createConnectionPool()
try:
  workers = startWorkers(pool)
  try:
    listener = openListener(workers)
    return Service(pool, workers, listener)
  catch error:
    stop(workers)
    raise error
catch error:
  close(pool)
  raise error

This pseudocode is intentionally explicit to show the ownership transitions. In production, structured cleanup primitives or a small lifecycle abstraction may express the same logic more safely.

The principle is that responsibility begins as soon as acquisition succeeds. A resource does not become somebody’s responsibility only after the entire constructor or startup sequence succeeds.

If ownership is transferred into a fully constructed Service, then Service owns the resources after construction. Before that transfer, the construction path owns each resource it has acquired and must unwind it on failure.

Cleanup should tolerate the lifecycle you actually support

Teams sometimes state that cleanup must be “idempotent,” meaning repeated cleanup calls have the same externally relevant effect as one call. That can be useful, but it is not a universal requirement.

Some resource APIs explicitly reject a second close. Others make it harmless. A component may need to report double shutdown because it indicates a programming error. The correct behavior depends on the contract.

What should be unambiguous is the supported lifecycle. For example:

created -> running -> stopped

If stop() may be called concurrently, the implementation needs synchronization appropriate to that guarantee. If concurrent calls are unsupported, document and enforce that assumption rather than accidentally relying on timing.

Similarly, cleanup can fail. Flushing buffered data, committing a final record, closing a network session, or waiting for a worker to terminate may produce an error. Decide whether the caller must observe that failure, whether shutdown should continue releasing remaining resources, and how multiple cleanup failures are reported.

A cleanup API that can fail should not be treated as ceremonial. Its failure semantics are part of the resource contract.

Do not hide long-lived resources behind innocent values

An API becomes difficult to use when an ordinary-looking value secretly keeps a scarce resource alive.

For example, returning a lazy sequence backed by a database cursor may look like returning a collection:

rows = findOrders(query)

If iterating rows requires an open cursor and connection, the value has a lifetime obligation that a normal in-memory collection does not have. A caller that reads only the first item and abandons the sequence may leave the underlying resource open until some unrelated cleanup mechanism runs.

There are several valid designs, depending on the workload:

  • materialize the data before returning when result sizes are safely bounded;
  • return an explicitly closeable reader or stream when incremental processing matters;
  • accept a callback or operation that runs while the resource is owned inside the function;
  • use the platform’s standard scoped resource abstraction when callers already understand its lifetime rules.

The trade-off is between convenience, memory use, streaming behavior, and lifecycle visibility. The important part is not to make a resource-backed value look indistinguishable from a resource-free value when callers must manage it differently.

Finalizers and garbage collection are not ownership models

In environments with garbage collection, unreachable memory may be reclaimed automatically. That does not mean every external resource has an acceptable automatic lifetime.

File descriptors, sockets, locks, worker threads, temporary files, and pooled connections can have limits or externally visible effects. Waiting for an unspecified future collection cycle may retain them longer than the application can tolerate. Some runtimes provide finalization as a fallback, but fallback cleanup does not remove the need for a deliberate lifetime when timely release matters.

The practical distinction is simple: memory reachability answers whether an object can still be used; resource ownership answers when an external or finite resource should be released. Those questions may coincide in some implementations, but software design should not assume they are identical unless the platform contract guarantees the behavior you need.

Common ownership mistakes

A few patterns deserve special attention.

A factory returns a live resource without exposing cleanup. Callers cannot tell that the returned value carries a lifetime obligation. Return a resource-aware abstraction or contain the lifetime inside the factory operation.

Every user tries to be helpful and close a shared dependency. Multiple borrowers then compete to end one lifetime. Keep one owner and make borrowers non-owning.

Cleanup exists only on the success path. Exceptions, cancellation, early returns, and partial startup bypass it. Put release logic on every supported exit path, preferably through structured cleanup mechanisms.

A top-level component knows how to close every nested detail. This breaks encapsulation and makes internal refactoring affect application shutdown. Let each owner release the resources it directly owns.

The design relies on process exit. That can be acceptable for some short-lived programs and resources when the operating environment guarantees adequate cleanup. It is a poor assumption for long-running processes, reusable libraries, graceful shutdown requirements, buffered output, or resources whose release has application-level meaning.

When a simpler approach is enough

Not every object needs an ownership abstraction. Plain values that hold only ordinary memory generally do not need explicit cleanup. A small function that opens a file, reads all required data, and closes it in one structured scope does not need a lifecycle framework either.

Add explicit lifecycle APIs when a resource must escape its acquisition scope, when components own nested long-lived resources, when shutdown order matters, or when failure during partial construction requires coordinated cleanup.

The goal is not to model ownership everywhere. It is to make lifetime responsibility visible exactly where the program can no longer infer it from a simple local scope.

Conclusion

When code acquires a finite or externally meaningful resource, ask who owns its lifetime before deciding where to put the cleanup call. Keep acquisition and release in one scope when possible. If the resource must escape, transfer ownership explicitly and expose how that lifetime ends. Let borrowers use shared resources without destroying them, and let each owner clean up the resources it directly owns.

This mental model turns cleanup from a scattered collection of close() calls into a design property. When ownership is clear, failure paths, shutdown order, shared dependencies, and refactoring all become easier to reason about.