> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Package service lifetime design

> Examine how bind, singleton, and scoped are implemented in Laravel 13, and learn how to design packages that do not carry state over in long-running workers and how to test lifecycle boundaries.

To make a package usable not only in regular HTTP requests but also in Octane and queue workers, you need to decide **how long a service lives and what it is allowed to hold**. If you store per-request data in a singleton, a different operation may end up reading stale data.

This page examines the container, queue worker, and Octane implementations as of Laravel Framework `v13.36.0`, and connects the basic registration methods of the [service container](/en/service-container) to the long-term maintenance of a package. Simply choosing `scoped()` does not reset every reference between objects or every piece of static state.

## Choose a registration method based on how long the service lives

| Registration | Retrieval with a regular `make()` | Suitable design |
| - | - | - |
| `bind()` | Creates a new instance on every resolution | Services that can be created per operation |
| `singleton()` | Reuses the created instance within the same container | Services that hold no request- or job-specific state |
| `scoped()` | Reuses the instance within the same lifecycle and removes the stored instance at the boundary | State shared within a single request or job |

This table compares regular resolution. Check special paths such as resolution with explicit parameters or contextual binding separately.

Even with `bind()`, if you store the retrieved service in a property of another singleton, that reference remains. Registration methods are rules for how the container creates and reuses objects; they do not force objects held by consumers to be recreated.

## scoped combines a shared registration with a record of what to remove

The `Container::scoped()` method we examined does two things:

1. It records the registration key in `scopedInstances`.
2. It calls `singleton()` and registers the service as a shared binding.

The instance itself is stored in the same `instances` array as singletons. `forgetScopedInstances()` calls `unset()` on the stored instances for the recorded keys. Because the binding's factory remains, the next regular resolution can create a new object.

```mermaid theme={null}
flowchart TD
    A["Register scoped in register()"] --> B["Record the factory and the key to remove"]
    B --> C["First make() in operation A"]
    C --> D["Create and store instance A"]
    D --> E["Retrieve the same instance within operation A"]
    E --> F["forgetScopedInstances()<br>at the lifecycle boundary"]
    F --> G["Remove the container's stored instance"]
    G --> H["make() in operation B creates instance B"]
```

This process does not reset the properties of the old object, nor does it rewrite references held by other objects. An old object that is still referenced can continue to be used.

### The runtime creates the boundary

The container itself does not watch for the start of HTTP requests or jobs.

* **Queue workers**: `QueueServiceProvider` passes a callback that resets the scope to the Worker. `Worker::daemon()` calls it in the loop before fetching the next job, and the callback performs tasks such as `forgetScopedInstances()` and clearing resolved Facade instances.
* **Octane**: The official default configuration registers `FlushTemporaryContainerInstances` for `OperationTerminated`. This listener calls `forgetScopedInstances()`.
* **Custom long-running commands or loops**: Simply calling `make()` repeatedly does not create a boundary. The runtime must design the lifecycle.

<Warning>
  Do not call `forgetScopedInstances()` on your own from regular application code or from a package's `boot()`. It affects every scoped service in the container. Also, not every job path, including synchronous execution, necessarily goes through the same reset path as a long-running Worker.
</Warning>

## Make state shared within an operation scoped

Consider a delivery package that wants to share a destination identifier within a single request or job. Rather than holding the HTTP Request directly, it stores only the value it needs.

```php theme={null}
<?php

namespace Acme\Courier;

final class DeliveryContext
{
    private ?string $destinationId = null;

    public function setDestinationId(string $destinationId): void
    {
        $this->destinationId = $destinationId;
    }

    public function destinationId(): ?string
    {
        return $this->destinationId;
    }
}
```

In the package's service provider, register an empty context without reading the consumer's state at creation time.

```php theme={null}
<?php

namespace Acme\Courier;

use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->scoped(DeliveryContext::class);
    }
}
```

In the consuming application's controller or job `handle()`, set an identifier that has already passed input validation and authorization checks. Other services that resolve this type within the same lifecycle can refer to the same context.

```php theme={null}
$context = app(\Acme\Courier\DeliveryContext::class);
$context->setDestinationId($authorizedDestinationId);
```

This context is not an authorization mechanism. Being able to set an identifier is not the same as being permitted to access that destination. Also, `scoped()` does not persist anything to a database or an external cache. Pass values needed by the next job explicitly as job data.

<Info>
  In Octane, a service provider's `register()` and `boot()` run when the worker first starts. Do not design your package to store the current user or destination in the context from `boot()`. Set per-operation values after the operation has started.
</Info>

## References from a singleton to a scoped service are not updated automatically

With the following registration, making only the context scoped does not work as intended.

```php theme={null}
$this->app->singleton(DeliveryService::class, function ($app) {
    return new DeliveryService($app->make(DeliveryContext::class));
});
```

If `DeliveryService` stores the context in a property in its constructor, it keeps holding the first context it was created with. Even if the container removes its stored context, the property of the already created `DeliveryService` does not change.

Choose a remedy that fits the service's responsibilities.

1. **Pass the values needed for each operation as method arguments**. If there is no need to share state, this is the simplest option.
2. **Make the holder scoped as well**. Recreate both `DeliveryService` and the context per operation.
3. **Pass a resolver that resolves at the time of use**. If the service must remain a singleton, retrieve the needed service from the current container each time.

In the third example, rather than capturing the old context in a closure, resolve it at call time.

```php theme={null}
use Illuminate\Container\Container;

$this->app->singleton(DeliveryService::class, function () {
    return new DeliveryService(
        fn () => Container::getInstance()->make(DeliveryContext::class),
    );
});
```

The `DeliveryService` in this example must be changed to accept a `Closure` in its constructor and expose an **API that calls it on every operation**. Calling the resolver only once in the constructor and storing the result does not solve the problem. This is a design example that applies the container resolver from the official Octane documentation to a package-specific context.

### Check for state that scoped does not clear

`scoped()` does not automatically clear the following state:

* Values added to the package's own static properties
* References to the context stored in another singleton or a global variable
* A "current user" or "current destination" held inside a singleton
* References held by custom Facades or cache layers

Distinguish between resets performed by the standard runtime and resets of the package's own state. Reducing the places that hold per-operation state is simpler than maintaining custom clearing logic for every runtime.

## Write tests that cross a boundary in the same container

If your HTTP tests only create a new application each time, you may miss problems that arise when a long-running process keeps using the same container. Start by verifying, with the container alone, that state is shared within an operation and recreated after the boundary.

The following tests use PHPUnit. Like the official Container tests, the first test uses `forgetScopedInstances()` to check the effect of the boundary without starting an entire Worker.

```php theme={null}
<?php

namespace Acme\Courier\Tests;

use Acme\Courier\DeliveryContext;
use Illuminate\Container\Container;
use PHPUnit\Framework\TestCase;

class ServiceLifetimeTest extends TestCase
{
    public function test_context_is_shared_then_recreated(): void
    {
        $container = new Container;
        $container->scoped(DeliveryContext::class);

        $first = $container->make(DeliveryContext::class);
        $first->setDestinationId('destination-a');

        $this->assertSame($first, $container->make(DeliveryContext::class));
        $this->assertSame('destination-a', $first->destinationId());

        $container->forgetScopedInstances();

        $second = $container->make(DeliveryContext::class);
        $this->assertNotSame($first, $second);
        $this->assertNull($second->destinationId());
    }

    public function test_singleton_keeps_a_captured_scoped_dependency(): void
    {
        $container = new Container;
        $container->scoped(DeliveryContext::class);
        $container->singleton('courier.holder', function ($app) {
            return (object) ['context' => $app->make(DeliveryContext::class)];
        });

        $holder = $container->make('courier.holder');
        $oldContext = $holder->context;

        $container->forgetScopedInstances();

        $this->assertSame($holder, $container->make('courier.holder'));
        $this->assertSame($oldContext, $holder->context);
        $this->assertNotSame($oldContext, $container->make(DeliveryContext::class));
    }
}
```

The second test documents the behavior where references remain. In a real package, also add tests that verify the remedy you adopted uses the new context or value.

| Check when updating | Expected result |
| - | - |
| Resolve the context multiple times within the same operation | The same instance and the values set are shared |
| Resolve across a boundary in the same container | A new instance that does not hold the previous values |
| A singleton uses the context | It does not store the old context and uses the current values |
| Run consecutive Octane requests in the consuming app | The second request does not inherit the first request's destination or other values |
| Run consecutive jobs in a long-running queue worker | Per-job values are isolated |
| Update Laravel or Octane | Recheck the reset call path and the consuming app's configuration |

Container-only tests do not verify Octane's sandbox or the actual execution path of queue workers. You also need integration tests that run consecutive operations in each runtime you claim to support. For deployments that change registration methods or service implementations, reload long-running processes so that the new code takes effect.

## Related pages

<Columns cols={2}>
  <Card title="Package service resolution hooks" icon="puzzle-piece" href="/en/advanced/package-service-resolution">
    Review afterResolving, retrieval of shared instances, and how configuration differs on re-resolution.
  </Card>

  <Card title="Laravel Octane" icon="bolt" href="/en/octane">
    Review caveats about holding the Request or container and operating long-running processes.
  </Card>
</Columns>

## Primary sources consulted

* [Laravel documentation: Binding scoped singletons](https://github.com/laravel/docs/blob/13.x/container.md#binding-scoped-singletons)
* [Laravel documentation: Dependency injection and Octane](https://github.com/laravel/docs/blob/13.x/octane.md#dependency-injection-and-octane)
* [Container: scoped(), singleton(), forgetScopedInstances(), and resolve()](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Container/Container.php)
* [ContainerTest: scoped sharing and recreation](https://github.com/laravel/framework/blob/v13.36.0/tests/Container/ContainerTest.php)
* [QueueServiceProvider: resetScope passed to the Worker](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Queue/QueueServiceProvider.php)
* [Worker: scope reset in daemon()](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Queue/Worker.php)
* [Octane: FlushTemporaryContainerInstances](https://github.com/laravel/octane/blob/ecc9254e6d79cd607f191015f634f9be944e2267/src/Listeners/FlushTemporaryContainerInstances.php)
* [Octane: default listeners for OperationTerminated](https://github.com/laravel/octane/blob/ecc9254e6d79cd607f191015f634f9be944e2267/config/octane.php)


## Related topics

- [Laravel Package Development](/en/advanced/package-development.md)
- [Advanced Topics](/en/advanced/index.md)
- [Package service resolution hooks](/en/advanced/package-service-resolution.md)
- [Guide to Building Apps with VOICEVOX Engine API](/en/packages/laravel-voicevox/app-guide.md)
- [Testing Laravel packages with Orchestra Testbench](/en/advanced/package-testing.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.