skip to content

Transaction Closures & Connections

config/database.php defines named connections with read/write hosts, and DB::transaction wraps work in a retrying closure. Interviewers probe sticky reads, rollback rules and after-commit hooks.

on this pageshow

explore

questions

6

In a fresh Laravel 13 app, which database connection is used by default, where is that decided, and how do you run a query on another named connection?

level: juniorimportance: must knowfreq 52%

answer

  1. config/database.php 'default' key
  2. DB_CONNECTION=sqlite in .env.example
  3. database/database.sqlite file
  4. DB::connection('pgsql')->table()
  5. connections array keyed by name

basics

~10 s

SQLite: config/database.php sets 'default' => env('DB_CONNECTION', 'sqlite') and the skeleton's .env.example sets DB_CONNECTION=sqlite, pointing at database/database.sqlite. DB::connection('pgsql') returns any other connection named in the 'connections' array.

solid answer

~30 s

In Laravel 13 the skeleton's `config/database.php` sets `'default' => env('DB_CONNECTION', 'sqlite')`, and `.env.example` ships `DB_CONNECTION=sqlite` with the MySQL lines commented out; the `sqlite` connection reads `DB_DATABASE`, falling back to `database/database.sqlite`. Every other connection lives in the `connections` array under a name (`sqlite`, `mysql`, `mariadb`, `pgsql`, `sqlsrv`, plus any you add). `DB::table()` and Eloquent use the default; `DB::connection('reporting')` returns the named connection, on which you call `table()`, `select()` or `transaction()`. An Eloquent model can pin its own connection with the `#[Connection('reporting')]` attribute or a `$connection` property. A single `DB_URL` can replace the separate host, port and credential variables.

code

php · 14 lines
php
<?php

use Illuminate\Support\Facades\DB;

// Default connection (SQLite in a fresh Laravel 13 app)
$members = DB::table('members')->count();

// A named connection from config/database.php 'connections'
$legacy = DB::connection('legacy')
    ->table('members')
    ->where('active', 1)
    ->count();

$name = DB::connection()->getName(); // 'sqlite' unless DB_CONNECTION says otherwise

go deeper

for a junior

Recall that DB_CONNECTION feeds config/database.php's default, that new apps use SQLite, and that DB::connection('name') picks another connection.

for a middle

Explain how named connections are created lazily and cached, how DB_URL replaces separate variables, and how a model pins its connection.

for a senior

Show that connections are independent, so transactions never span them, and diagnose environment changes hidden by cached configuration.

for a principal

Weigh whether a second database belongs behind its own connection in this app or behind a separate service boundary.

## Where the default comes from Laravel reads its database settings from **`config/database.php`**. Two keys matter most: - **`default`** names the connection used whenever code does not ask for a specific one. In the Laravel 13 skeleton it is `env('DB_CONNECTION', 'sqlite')`. - **`connections`** is an array of named connection definitions. The skeleton ships `sqlite`, `mysql`, `mariadb`, `pgsql` and `sqlsrv`, each reading its details from environment variables. The skeleton's `.env.example` sets `DB_CONNECTION=sqlite` and leaves `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME` and `DB_PASSWORD` commented out. The `sqlite` connection uses `DB_DATABASE` when set and otherwise `database_path('database.sqlite')`, a file the installer creates. So a brand-new app runs migrations and serves requests with no database server at all. ## Switching the default To move an app to PostgreSQL, change the environment, not the code: ```ini DB_CONNECTION=pgsql DB_HOST=127.0.0.1 DB_PORT=5432 DB_DATABASE=coffee DB_USERNAME=app DB_PASSWORD=secret ``` Every connection also reads **`DB_URL`**. When a managed provider hands you one URL such as `pgsql://app:[email protected]:5432/coffee`, setting `DB_URL` supplies the driver, host, credentials and database in one variable. Because these values reach the app through `config/database.php`, a production deploy that caches configuration must re-cache after changing them. ## Using a second connection Many apps talk to more than one database: a reporting replica, a legacy system, a separate analytics store. Add a named entry to `connections`, then ask for it by name: ```php use Illuminate\Support\Facades\DB; $rows = DB::connection('legacy')->table('members')->where('active', 1)->get(); DB::connection('legacy')->transaction(function () { // statements here run on the legacy connection only }); ``` Useful facts about `DB::connection()`: 1. **Called without a name**, it returns the default connection. `DB::table()` is shorthand for `DB::connection()->table()`. 2. **Connections are created lazily** and cached by name for the rest of the process, so asking twice returns the same object and does not reconnect. 3. **Each connection is independent**: a transaction opened on the default connection does not cover statements sent through `DB::connection('legacy')`. 4. **An unknown name throws** an `InvalidArgumentException`, which surfaces a typo at the first query. ## Eloquent models A model uses the default connection unless told otherwise. In Laravel 13 the idiomatic way to pin one is the `Illuminate\Database\Eloquent\Attributes\Connection` attribute, `#[Connection('legacy')]`, placed on the class; the older `protected $connection = 'legacy';` property still works. ## Pitfalls around connections - **SQLite in development, MySQL in production.** The default makes a fresh app easy to start, but SQL that works on SQLite, such as date functions or loose type comparisons, can behave differently on the production driver. Run the test suite against the production driver before relying on it. - **Foreign keys on SQLite.** The skeleton's `sqlite` connection enables foreign key constraints through `DB_FOREIGN_KEYS`, which defaults to `true`; turning it off hides integrity errors that MySQL or PostgreSQL would raise. - **Hard-coded names.** Code that calls `DB::connection('mysql')` breaks when the default moves to `pgsql`. Use the default connection unless the second database is genuinely separate, and name that one after its role, such as `legacy` or `reporting`. ## Summary | Question | Answer in Laravel 13 | |---|---| | Default driver in a new app | SQLite | | Where it is chosen | `config/database.php` `default`, from `DB_CONNECTION` | | SQLite file location | `database/database.sqlite` unless `DB_DATABASE` is set | | Query another connection | `DB::connection('name')->table(...)` | | Pin a model to a connection | `#[Connection('name')]` or `$connection` | ## Why interviewers ask The question checks that a candidate knows configuration flows from environment variables through `config/database.php`, that the default changed to SQLite for new apps, and that a connection is a named, independent object. That last point matters later when transactions, read replicas and multi-database reports enter the conversation.

  • You set DB_CONNECTION=pgsql in production but queries still hit SQLite. What would you check first?
    Whether configuration is cached: after `php artisan config:cache`, the cached array is used and `.env` changes are ignored until the cache is rebuilt. Then check that `DB_CONNECTION` is set in the real environment of the PHP process, and that `config('database.default')` reports `pgsql` in `php artisan tinker` on that server.
  • Does DB::transaction() on the default connection protect writes made through DB::connection('legacy')?
    No. A transaction belongs to one connection object and its PDO handle. Writes sent through another named connection run in that connection's own autocommit mode or its own transaction. If both must succeed together, you need a design that tolerates partial failure, because Laravel does not coordinate transactions across connections.

saying these in an interview costs you the question

  • Says a new Laravel 13 app defaults to MySQL
  • Edits config/database.php values instead of environment variables
  • Thinks DB::connection('x') opens a new PDO on every call
  • Believes one DB::transaction() spans all configured connections
  • Expects a missing connection name to fall back to the default
open as a page

In Laravel, how does DB::transaction() with a closure differ from DB::beginTransaction(), commit() and rollBack() when transferring loyalty points between two members?

level: middleimportance: must knowfreq 60%

basics

~20 s

DB::transaction() begins, runs the closure, commits on success, and on any exception rolls back and rethrows, returning the closure's value. The manual trio leaves every path to you, so a missed rollBack() leaves the transaction open on the connection.

open as a page

In Laravel, how do you log every SQL query with DB::listen(), and how does DB::whenQueryingForLongerThan() differ for spotting slow requests?

level: middleimportance: should knowfreq 30%

basics

~10 s

DB::listen() registers a closure that receives a QueryExecuted event after every query, with sql, bindings, time in milliseconds and connectionName. whenQueryingForLongerThan() instead fires once when a connection's total query time exceeds a threshold.

open as a page

In Laravel, when does a DB::afterCommit() callback registered during a loyalty-points transfer run, and what happens to it if the transaction rolls back?

level: seniorimportance: should knowfreq 32%

basics

~10 s

It runs right after the outermost transaction commits, or immediately if no transaction is open. If the transaction, or the savepoint it was registered in, rolls back, the callback is discarded and never runs.

open as a page

In Laravel's config/database.php, how do read and write hosts and the sticky option route queries, and why might a member not see points just transferred?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Selects use a read host and writes use the write host. Without sticky, a read after a write in the same request can hit a lagging replica and miss the new points; reads inside a transaction always use the writer.

open as a page

In Laravel, what does DB::transaction($callback, attempts: 5) actually retry, and what can go wrong when the loyalty-transfer closure runs a second time?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Only concurrency errors are retried: SQLSTATE 40001, deadlock and lock-wait-timeout messages, or SQLite's 'database is locked'. Any other exception is rolled back and rethrown at once. A retry re-runs the whole closure, so non-database side effects inside it happen again.

open as a page