cswork / libraryOpen practice workspace

Articles

Symfony Interview Questions: Autowiring, Service Decoration, and Container Debugging

Practice Symfony interview questions on autowiring, named aliases, service decoration order, and container debugging with concrete service-graph traces.

Symfony container interviews are easier when you draw the object graph before explaining framework terms. Autowiring selects a service from a type and configured aliases; decoration replaces the public service path with wrappers; debugging asks where the resolved graph differs from the intended one. A strong answer says which object is injected, in which order wrappers run, and which command would prove the claim. This guide uses original interview exercises grounded in Symfony's current autowiring documentation, not reported employer prompts.

The examples are illustrative and were not executed in a local Symfony application. Syntax and deprecations depend on the Symfony version in your project, so check the installed docs before pasting an example. For related dependency-injection practice, start with Explain Spring Dependency Injection and Choose an Injection Style, then translate its graph reasoning into Symfony's service IDs and aliases.

Question 1: What does autowiring actually resolve?

Suppose ReportController asks for ExporterInterface, and both CsvExporter and JsonExporter implement it. “Symfony picks one” is not a sufficient answer. Autowiring reads type hints and finds a resolvable service or alias; when the choice is unclear, the container should fail with a useful error rather than guess based on class names. The official autowiring guide explains that services commonly use their fully qualified class name as the ID and that interface types need an unambiguous mapping.

In an interview, write the intended contract first: ExporterInterface has a default implementation for ordinary reports, while one invoice path explicitly needs JSON. Then show the two bindings. A default alias can point ExporterInterface to CsvExporter. A named autowiring alias can point ExporterInterface $invoiceExporter to JsonExporter. The controller's dependency must select the named alias deliberately, for example with #[Target('invoiceExporter')] in Symfony versions that support it. Do not say #[Target] takes any service ID; current docs say it names a named autowiring alias.

# Illustrative services.yaml fragment; namespace and imports are omitted.
services:
  App\Export\ExporterInterface: '@App\Export\CsvExporter'
  'App\Export\ExporterInterface $invoiceExporter': '@App\Export\JsonExporter'
use App\Export\ExporterInterface;
use Symfony\Component\DependencyInjection\Attribute\Target;

final class InvoiceController
{
    public function __construct(
        #[Target('invoiceExporter')]
        private ExporterInterface $exporter,
    ) {}
}

The exact namespaces and service loading must match the application. The important graph is InvoiceController → named interface alias → JsonExporter; another consumer of the bare interface follows the default alias to CsvExporter. If the JSON service is not registered, the named alias alone does not create it. If the default alias is absent, a consumer with an unqualified interface may fail even though the named path works.

An older-looking pattern is to rely on the PHP parameter name matching a named alias. Current Symfony documentation recommends #[Target] because renaming the variable can silently change the injected implementation to the default. It also documents version-sensitive deprecation of name-only matching. In a real interview, state the installed Symfony version before claiming an attribute is available, and prefer an explicit binding that makes refactors safe.

Question 2: How do you distinguish a missing service from an ambiguous one?

Do not start by editing services.yaml blindly. Read the full compile error: which consumer, constructor argument, type, and candidate service IDs are named? Then run php bin/console debug:autowiring ExporterInterface to see autowirable types and aliases. Run php bin/console debug:container 'App\Export\JsonExporter' to inspect the concrete service; if the service ID is private or optimized away, use the command's options and the relevant environment to inspect what the compiled container knows. The container documentation and command help are the authority for your installed version.

If an interface has no default alias, add the one the application actually intends. If the implementation class is outside the service-discovery path, registering an interface alias to that missing ID only moves the error. If a scalar such as an API endpoint string cannot be autowired, configure it explicitly through a parameter, environment binding, or documented attribute; do not invent a class service to conceal the missing configuration.

The diagnostic sequence should be falsifiable. “I suspect the service is absent because debug:container cannot find its ID in the test environment” is a hypothesis with a check. “Autowiring is broken” is not. Compare the dev and prod compiled containers when the issue is environment-specific, and clear/rebuild the appropriate cache only after identifying a plausible stale artifact. A cache clear is not evidence that the wiring design is correct.

Question 3: What does decoration change?

Imagine PriceCalculator implements PriceCalculatorInterface. You add LoggingPriceCalculator to record calls and CachingPriceCalculator to avoid repeated work. A decorator is not merely another implementation competing for the interface. It wraps an existing service: the decorated service ID resolves to the outer wrapper, and the original object is injected as the wrapper's inner dependency under an inner service ID. The Symfony service-decoration guide describes that transformation.

Draw the call path and the return path separately. If the chain is Logging → Caching → Original, calling the public ID enters Logging first; Logging calls Caching; Caching either returns a hit or calls Original. On a cache hit, Original is skipped, but Logging still observes the request. If the chain is Caching → Logging → Original, a cache hit skips Logging too. That is not a cosmetic reordering: it changes observability. The requested behavior should determine the order.

Consumer → public PriceCalculatorInterface alias
         → LoggingPriceCalculator
         → CachingPriceCalculator
         → original PriceCalculator

Use a wrapper that implements the same contract. Inject the inner service into the wrapper's constructor and delegate every method the contract requires. If a decorator forgets one method, changes exception behavior, or caches responses without including relevant inputs in the key, the container may compile while behavior is wrong. A service graph explains which objects exist; behavior tests explain whether the wrappers preserve the promised contract.

Question 4: Which decorator runs first when priorities differ?

Symfony supports decoration_priority. Its documentation says higher-priority decorators are applied earlier and end up closer to the original service; lower-priority decorators become the outer wrappers that are entered first. This can sound backward if you equate “applied first” with “called first.” Explain it with an object-construction trace rather than memorizing a slogan.

Suppose Caching has priority 20 and Logging has priority 0. Start with Original. Apply the higher-priority Caching wrapper around Original. Then apply Logging around that result. The consumer therefore calls Logging, then Caching, then Original on a miss. On a cache hit, Logging and Caching run but Original does not. If the intended policy is “log only underlying calculations,” this arrangement is wrong; move the logging concern closer to Original or change where logging occurs.

PriorityWrapper after constructionEntered on a cache missEntered on a cache hit
20: CachingClosest to OriginalAfter LoggingAfter Logging
0: LoggingOutermostFirstFirst
OriginalInnermostLastNot called

This table is a worked example, not a universal recommendation for these two concerns. Real decorators may also transform errors or metrics, which changes the desired placement. Test the contract with a fake inner service that records calls: one miss, one hit, one thrown exception. The test should assert the call sequence, not merely a successful response.

Question 5: What if the public alias points somewhere unexpected?

Trace by ID rather than by class name. The consumer type hint may resolve to an interface alias. That alias may refer to a decorated service ID, not directly to the original concrete object. A concrete class can also be registered under a custom ID. If you only inspect the class file, you can miss the alias and decorator layers that determine runtime behavior.

Start with the consumer's exact type and attribute. Ask the container which alias is autowirable. Inspect the target ID and any decoration metadata in the compiled container. Use php bin/console debug:container with the relevant ID and the same environment as the failure. If a wiring error appears only during compilation, php bin/console lint:container may help, but passing a lint command does not prove the wrapper order or business behavior. Confirm those with a small integration or functional test.

Be precise about visibility. A private service is not necessarily unavailable to constructor injection; it may be inlined or removed as an independent runtime entry after compilation. “I cannot fetch it directly from the container” is not equivalent to “Symfony cannot inject it.” Describe whether you are diagnosing service registration, autowiring eligibility, public lookup, or runtime behavior. Each has a different probe.

A full debugging exercise

An interviewer says: “Invoice exports switched from JSON to CSV after we renamed $invoiceExporter to $exporter. The app still boots. Why?” A plausible cause is reliance on parameter-name-based named alias selection. The renamed parameter no longer matches the JSON alias, so the default interface alias supplies CSV. Do not assert that without checking the effective service graph; another explicit binding could override the name. Inspect the constructor, debug:autowiring output, and relevant service definition. A robust fix uses an explicit named target or a dedicated interface where the distinction is truly semantic.

Now add a second symptom: exports are correctly JSON, but a new logging decorator reports no calls for repeated invoices. The graph may put caching outside logging, so cache hits return before the logging wrapper. Inspect decoration priorities and run a two-call test using the same cache key. The two symptoms have different causes. The first is alias selection; the second is wrapper order. Changing both configurations at once makes it harder to prove either diagnosis.

Finish by asking what would break in production. A decorator might log sensitive invoice contents, a cache might mix tenants if the key omits tenant identity, and a missing inner-service delegation might change errors. The framework mechanics are only valuable when tied to the actual contract and risk. An excellent answer names what the graph guarantees and what still requires behavior tests.

The interview-ready answer

Lead with the requested object graph: consumer type, alias, concrete service, and any wrappers. Explain which part is a compile-time resolution and which is runtime behavior. Use debug:autowiring for eligible types, debug:container for service IDs, and a focused test for call order. If the installed version differs from current online docs, adapt the syntax to that version and say so. The Symfony service-container docs are the place to verify exact commands and features.

The key habit is to make every container claim inspectable. A graph can show why the wrong exporter was injected; a two-call trace can show why logging disappeared on cache hits. Those are the artifacts an interviewer can challenge and a production team can use to repair a real problem.

Sources and Further Reading

Research note: Technical claims were checked against current Symfony documentation September 27, 2026. The invoice service and interview questions are original illustrative exercises, not executed framework tests or employer reports.