An interface cannot be built, so it needs a binding. The trace followed a provider's $singletons entry (Writing Service Providers), shorthand for $this->app->singleton(PaymentGateway::class, FakeGateway::class). The method sets the lifetime, which FakeGateway, counting its charges, makes visible:
use App\Contracts\PaymentGateway;
use App\Services\FakeGateway;
foreach (['bind', 'singleton', 'scoped'] as $method) {
app()->$method(PaymentGateway::class, FakeGateway::class);
$charge = fn () => app(PaymentGateway::class)->charge(10, 'test');
$refs = [$charge(), $charge()];
app()->forgetScopedInstances(); // what Octane and queue workers do
$refs[] = $charge();
printf("%-9s %s\n", $method, implode(' ', $refs));
}bind fake_0001 fake_0001 fake_0001 singleton fake_0001 fake_0002 fake_0003 scoped fake_0001 fake_0002 fake_0001
bind() builds anew on every make(), singleton() keeps one object per process, and scoped() one per request or job, since Octane 4,043 and queue workers flush scoped instances between units of work. Prefer scoped() for per-request state. instance() registers an object you built.
Since Laravel 12 2,157 the interface can carry its binding (Controller Injection). With #[Bind(StripeGateway::class)], #[Bind(FakeGateway::class, environments: ['local', 'testing'])] and #[Singleton] on PaymentGateway and no provider, local resolved a shared FakeGateway and production a StripeGateway. #[Scoped] exists too, and #[BindWhen] takes a closure, which needs PHP 8.5. Explicit bindings override attributes.