Interview guides · Aussie Broadband
Aussie Broadband Software Engineer Interview Guide
Aussie Broadband Software Engineer interview guide: PHP and Laravel questions, N+1, queue workers, take-home, values stories and a 7-day plan.
The 60-second brief
Aussie Broadband is an Australian telecommunications provider. Candidates describe the Software Engineer role as work on both internal tools and customer-facing applications: Laravel backend services, API integrations, and automation such as application ordering and queue management. Day to day that means turning product requirements into working PHP code, reviewing code with other engineers, and helping troubleshoot production issues.
This guide is for candidates applying for a Software Engineer position at Aussie Broadband. It covers the nine questions candidates report (five technical, four behavioral), the stages they describe, and a seven-day plan built around PHP, Laravel, the N+1 problem, queue workers and scheduled jobs.
Candidates describe an initial phone or video screen, a take-home technical project, a technical discussion in which you explain that project, and final interview rounds. No named rounds beyond that are reported, so confirm the exact stages with your recruiter.
Focus areas
- PHP
- Laravel
- n+1 Problem
Overview
The work candidates describe is backend web development in PHP with Laravel: building and maintaining APIs, integrating with other systems, and automating processes such as application ordering and queue management. The requirements candidates list put PHP, Laravel, API design and database optimisation first, with queue workers, job scheduling and CI/CD as extras. Your preparation should therefore be hands-on: you will do better explaining code you have just written than reciting definitions.
The reported questions fall into two groups. The technical group is mostly conceptual PHP and web-development questions (the N+1 problem, type-hinting, what clean code means, what PHP could improve on) plus one hands-on build: two Laravel API endpoints, one listing applications and one automating order processing, with queue workers and scheduling. The behavioral group covers your background, a mistake and how you fixed it, your favorite company value, and where two randomly selected values applied in a previous role. Half of that group depends on knowing the company's stated values before you walk in.
Candidates describe a take-home technical project followed by a discussion where you explain your choices, and mention refactoring a provided snippet for performance or readability as an example scenario. That shapes how to build: keep a short written record of each design decision and its trade-off while you work, so the follow-up conversation is a walk through your own reasoning rather than a reconstruction.
The plan points at them on the days where they reinforce N+1 diagnosis, order-state updates and retry-safe jobs.
Where to focus your preparation
Candidates describe the stages below in this order, but no round names or counts are reported, so treat this as a map of what to prepare and confirm the real format with your recruiter.
Preparation focus
Candidates describe a multi-stage process: an initial screen by phone or video, a take-home technical project, a technical discussion where you explain and defend what you built, and final interview rounds with several team members. The take-home is described as the main gate, and values questions appear alongside the technical material. Approach it as one continuous story: the code you submit is the material for the discussion, and your values stories are the material for the culture questions. Ask the recruiter which stages apply to you and whether you will write code live.
What to demonstrate
- Working PHP and Laravel code you can explain line by line, including why you structured it that way
- Trade-off reasoning on your own take-home: what you chose, what you left out and why
- Whether you can tie past work to the company's stated values with concrete outcomes
- Database and application performance thinking, such as spotting N+1 queries in a snippet
How to prepare
- Build a small Laravel app with a paginated applications listing and an order-processing endpoint that dispatches a queued job, plus one scheduled command, and write a README of design decisions
- Take a controller you wrote and refactor it for readability, then take a slow listing and fix it for query count; practise explaining both changes aloud
- Read the company's published values and write one real story per value, with the outcome
- Ask your recruiter which stages apply, whether a take-home is included, and what the technical discussion covers
Where candidates lose points
Defining the N+1 problem in the abstract without showing the query pattern or the fix
Practise with a real Laravel listing: count the queries for a list of applications and their related records, fix it with eager loading, and show the count dropping. Be able to say when a join or a counted subquery is better than loading whole relations.
Submitting a take-home that works on the happy path only
For the listing and order-processing endpoints, add request validation, pagination, a queued job with retry limits and backoff, a failed-jobs path, a guard against the same order being processed twice, and a few feature tests. Cover these before polishing anything else.
Being unable to explain your own take-home choices in the technical discussion
Keep a decision log in the README while you build: each choice, the alternative you rejected and the trade-off. Re-read it before the discussion and rehearse defending the two choices you are least sure of.
Answering the values questions with slogans and no story
Read the published values early, then write one concrete story per value with a stated outcome. Rehearse aloud so that the two-random-values question does not leave you building a story on the spot.
Treating the question about what PHP could improve as a chance to bash the language, or saying nothing needs improving
Prepare a balanced answer: one or two specific weaknesses (for example historical inconsistency in standard-library naming), what modern PHP has done about it through typed properties, enums and stricter types, and how you work around what remains.
Questions & practice
Collapse a redelivered event batch into per-aggregate high-water marks
Easy · hashing · at-least-once · aggregation
You drain a batch of up to 5,000,000 events, each (aggregate_id BIGINT, aggregate_version INT, event_type, payload). The log guarantees order within one aggregate only; the batch merges 64 partitions, and a relay failover has redelivered a range, so an older version for an aggregate can appear after a newer one. Given a map of last_applied_version per aggregate, produce the events worth applying, at most one per (aggregate_id, version), plus the count discarded. Target O(n) time. State the memory for 2,000,000 distinct aggregates and what you do when it does not fit.
Approach
- One pass, one hash map from aggregate_id to the highest version kept, and a discard counter. An event whose version is at or below last_applied_version for its aggregate is dropped without further work, which is the whole reason the event carries its version rather than a delta. O(n) expected time, O(d) space in distinct aggregates.
- Keep the maximum, never the last occurrence. The redelivered range means the final appearance of an aggregate in the batch can be an older version than one seen earlier in the same batch, so last-wins applies stale state over newer state and the projection regresses with no error anywhere.
- Cost the memory instead of calling it large: an 8-byte key plus a 4-byte version is 12 bytes of payload, and an open-addressed table held at a 0.7 load factor costs roughly 17 bytes per entry before per-slot metadata, so 2,000,000 aggregates is tens of megabytes in a native layout and several times that in a runtime that boxes both key and value.
- If the distinct set exceeds memory, partition on hash(aggregate_id) mod P and reduce each partition independently. Every event for one aggregate hashes to the same partition, so the per-partition result is exact and the merge is concatenation rather than a second reduction.
Follow-up
- The payload is a patch rather than a snapshot, so applying only the highest version loses the intermediate changes. What changes in your reduction?
- How do you detect that version 7 arrived while version 6 was never delivered, and what should the consumer do about the gap?
Identify the heaviest tenants in a five-minute window under memory pressure
Medium · top-k · heavy hitters · streaming
The edge service handles about 3,000 requests per second across roughly 50,000 tenants, peaking near 9,000. Expose the 50 heaviest tenants by request count over the trailing five minutes so limits can be tightened before one tenant's backfill starves the fleet. You may not retain five minutes of raw records. Give the exact solution and its memory, then the bounded-memory approximation with its error stated as a formula, and say which you would ship and at what tenant cardinality that choice changes.
Approach
- Do the exact version first, because it is affordable at this cardinality: a ring of 300 one-second counters per tenant, advanced lazily, is 1,200 bytes of counters per tenant and roughly 60 to 90 MB for 50,000 tenants with overhead. Carry a running total and subtract the bucket you overwrite so a window read is O(1) rather than 300 adds.
- Extract the top 50 with a size-k min-heap over the tenant sums: O(d log k) for d tenants, against O(d log d) to sort them all. Maintaining the heap continuously instead of on query requires a tenant-to-heap-index map, because incrementing a count already inside the heap means sifting from a known position, and without that map you rebuild the heap on every request.
- State the approximation precisely rather than gesturing at sketches. Misra-Gries with m counters retains every item whose true count exceeds N/(m+1), and each retained count underestimates the truth by at most N/(m+1). With m = 1,000 and N = 900,000 requests in the window the error is roughly 900 requests, which is fine for spotting a tenant sending 50,000 and useless for ranking two tenants 200 apart.
- Say what breaks when the window slides: Misra-Gries and Space-Saving are insert-only and cannot be decremented as records age out. The workable construction is one summary per sub-window, say ten seconds, with 30 summaries merged at query time, and the merged error is the sum of the per-summary errors, so the bound degrades linearly in the number of sub-windows.
Follow-up
- The heaviest tenant is heavy because of one export job rather than user traffic. Should the limiter treat those as the same tenant?
- Two tenants sit tied at the boundary of the top 50. Does your answer flap, and does the flapping matter?
Track a rolling failure rate per destination for circuit decisions
Easy · sliding window · ring buffer · circuit breaker
The egress service delivers about 1,500 webhooks per second across roughly 40,000 destinations, each call bounded by a 10 second timeout. Maintain, per destination, the failure rate over the trailing 60 seconds so a caller can ask before dispatch whether the circuit should open. Attempts arrive as (destination_id, finished_at_ms, outcome). Requirement: amortised O(1) per attempt, with total memory bounded by the destination count rather than by traffic. Give the structure, its exact memory, and the rule that stops a destination with three attempts from opening a circuit.
Approach
- Name the exact-deque version and then reject it as the default. Holding timestamps and advancing a tail pointer past anything older than now minus 60 seconds is a correct two-pointer window at amortised O(1) per attempt, but its memory tracks in-window traffic, so one destination in a retry storm holds hundreds of thousands of entries while thousands of quiet destinations hold none.
- Use a ring of 60 one-second buckets per destination, each bucket a pair of counters for attempts and failures. On an attempt, advance the ring by the elapsed whole seconds, zeroing at most min(elapsed, 60) buckets, then increment the head. That is amortised O(1) with a fixed footprint per destination.
- State the footprint: 60 buckets times two 4-byte counters is 480 bytes of payload per destination, so 40,000 destinations is roughly 20 to 25 MB with per-entry overhead, bounded by the catalogue rather than by the rate. The cost is granularity, since the oldest bucket ages out in whole seconds, which is far tighter than the decision needs.
- Require a minimum sample before the circuit may open. A destination with three attempts and three failures reads as 100 percent and is not evidence; a floor of roughly 20 attempts in the window makes the ratio meaningful, and below that floor use a run of consecutive failures as the trigger instead.
Follow-up
- The fleet is 30 instances and each sees roughly a thirtieth of a destination's traffic. Where does the rate actually live, and what does a per-instance answer get wrong?
- A destination answers in 9.5 seconds and succeeds. It is not failing but it is consuming your per-destination concurrency. What signal should open the circuit here?
Denormalise tenant onto revisions and backfill it live
Hard · denormalisation · online migration · backfill
resource_revision (revision_id, resource_id, version, actor_user_id, change_kind, patch, request_id, created_at) has 400M rows and no tenant column; tenant_id lives only on resource. Two reads need it: a tenant-scoped audit feed ordered by created_at DESC, and an offboarding purge. Both join back to resource today. Justify adding tenant_id to resource_revision against those two reads, name the anomaly the copy introduces and the constraint that prevents it, then give the ordered migration for a live table taking 1.2k writes/second — the lock each step takes, how the backfill is batched, and where each step stops being reversible. PostgreSQL 16.
Approach
- Justify from the access path rather than from taste. Without the column, the audit feed either scans resource_revision by created_at and discards other tenants' rows, or resolves the tenant's resource_ids first and probes with them — both proportional to the tenant's whole history rather than to one page. With (tenant_id, created_at DESC, revision_id DESC) it is a seek that stops at 50 rows, and the purge becomes a ranged delete instead of a join.
- Name the cost exactly: a second copy of a fact can disagree with the first. Make the disagreement unwritable rather than documented — add UNIQUE (resource_id, tenant_id) on resource so it can serve as a foreign-key target, then FOREIGN KEY (resource_id, tenant_id) REFERENCES resource (resource_id, tenant_id) on the revision table. A revision can then only ever carry its parent's tenant.
- Step one, expand: ALTER TABLE resource_revision ADD COLUMN tenant_id BIGINT NULL, with no default, so it is a catalogue change and no rewrite. It still needs ACCESS EXCLUSIVE for an instant, and that instant queues behind the longest open transaction on the table while every later query queues behind it — set lock_timeout to 2s and retry rather than wait.
- Step two, dual-write: deploy the writer that populates tenant_id on every new revision while reads still use the join. Reversible by redeploying the previous build, because nothing reads the column yet.
Follow-up
- The backfill is half finished and a rollback is required. What state is the table in, and what does the previous build do with a half-populated column?
- How do you verify the backfill actually finished, given rows are still being inserted while it runs?
Write the update path that detects a concurrent edit
Medium · optimistic concurrency · transactional outbox · audit history
resource carries version INT NOT NULL DEFAULT 1. resource_revision holds revision_id, resource_id, version, actor_user_id, change_kind, patch JSONB, request_id, created_at with UNIQUE (resource_id, version). outbox_event holds aggregate_type, aggregate_id, aggregate_version, event_type, payload, status. A PUT carries the version the client read. Write the exact statements for the single transaction that applies the edit, records the revision and enqueues 'resource.updated', and give the handler's branch on zero affected rows. Then say what PostgreSQL 16 does under READ COMMITTED when two of these updates hit one row at once.
Approach
- One transaction, three writes, no network call inside it: UPDATE resource SET title = $3, version = version + 1, updated_at = now() WHERE resource_id = $1 AND tenant_id = $4 AND version = $2; then INSERT the resource_revision row at version $2 + 1; then INSERT the outbox_event row at the same aggregate_version. The event goes to a table rather than a broker because no transaction spans both.
- Branch on the affected-row count before doing anything else. Zero has three causes — stale version, wrong tenant, row gone — so re-read once and map to 409 carrying the current version, or 404 for an id outside the caller's tenant, which also stops the endpoint confirming that another tenant's id exists.
- State the engine behaviour instead of assuming it. Under READ COMMITTED the second UPDATE blocks on the row lock, and when the first commits PostgreSQL re-evaluates the WHERE clause against the newly committed row, so the version predicate now fails and the statement reports zero rows. Under REPEATABLE READ the identical collision raises SQLSTATE 40001 instead, so the handler must fold both shapes into one conflict response.
- Keep UNIQUE (resource_id, version) even though the predicate already serialises writers. It is what makes a lost update unwritable if any other path ever reaches the revision table, and it converts a logic bug into 23505 rather than into a silently missing history row.
Follow-up
- A client sends the version it read ten minutes ago and the resource has moved three versions. What is in your 409 so it can resolve the conflict without a full re-fetch?
- Two editors, two disjoint fields, no overlap. Does your answer still refuse the second write, and should it?
What is the N+1 problem, and how would you tackle it?
Medium · Technical and Domain Knowledge
Approach
- Define it with a concrete Eloquent case: Order::all() runs one query, then reading $order->customer inside a loop lazy-loads each relation, so N orders cost 1 + N queries. The cost grows with the row count, which is why it hides in development and appears on real data.
- The standard fix is eager loading: Order::with('customer')->get() runs two queries (orders, then customers WHERE id IN (...)), whatever N is. Nested relations use dot syntax, with('items.product'), adding one query per relation level.
- Cover the variants: $orders->load('customer') when the collection is already fetched, withCount('items') or withSum() instead of loading a whole relation to count it, and constrained eager loads such as with(['items' => fn ($q) => $q->where('status', 'active')]).
- Say how you find it: Laravel Debugbar or Telescope query counts per request, DB::listen in a test, and Model::preventLazyLoading(! app()->isProduction()) in a service provider, which throws on any lazy load outside production.
- Name the trade-offs: eager loading a large relation can cost more memory than it saves, so select only needed columns, paginate the parent query, or use a join or aggregate when you need one value rather than models.
Follow-up
- How would you write a test that fails if this endpoint starts issuing one query per row again?
- When would you choose a join over eager loading, and what changes in the Eloquent models you get back?
- The IN list for eager loading grows to tens of thousands of ids. What do you do?
What is type-hinting, and why is it useful?
Medium · Technical and Domain Knowledge
Approach
- Define it for PHP: declared types on parameters, return values and (since 7.4) class properties, checked by the engine at runtime. Name the milestones: scalar and return types in 7.0, nullable ?int and void in 7.1, union types and mixed in 8.0, intersection types and never in 8.1.
- Explain declare(strict_types=1): it is per file and governs calls made from that file. Without it PHP coerces scalars ("5" passed to an int parameter becomes 5); with it the same call throws a TypeError.
- Give the benefits concretely: bad input fails at the boundary with a TypeError instead of corrupting data three calls later, IDEs autocomplete and rename safely, and static analysers such as PHPStan or Psalm catch mismatches before the code runs.
- Tie it to Laravel: the service container autowires constructor dependencies from their type hints, route model binding resolves a type-hinted Eloquent parameter from the URL, and a type-hinted Form Request is validated before the controller method runs.
- State the limits: PHP has no runtime generics, so array<int, Order> or Collection<int, Order> lives in PHPDoc and is checked only by static analysis, and a mixed or untyped array still needs validation.
Follow-up
- What exactly does strict_types change, and does it affect a call into your file from a file without it?
- How would you add types to a large untyped legacy codebase without breaking it?
- How do you express "a collection of Order models" when PHP itself cannot check it?
What would you describe as clean code?
Medium · Technical and Domain Knowledge
Approach
- Give a working definition rather than a slogan: code a colleague can read once, change safely and test without setting up the whole application. Then back each part with a concrete practice.
- Name the practices: names that state intent ($overdueInvoices, not $data), small functions with one reason to change, no hidden side effects, early returns instead of deep nesting, and comments that explain why, not what.
- Make it Laravel-specific: thin controllers, validation in Form Requests, business logic in service or action classes, slow work moved into queued jobs, and API Resources so the response shape is not built inline.
- Show how you keep it enforced instead of hoped for: an agreed style (PSR-12 via Laravel Pint or PHP-CS-Fixer), PHPStan in CI, feature tests on endpoints, and review comments that point at behaviour rather than taste.
- Name the trade-off: removing duplication too early produces abstractions nobody can follow, so I wait until the third repetition, and I prefer a short explicit function over a clever one-liner.
Follow-up
- Show me a piece of code you refactored for readability. What did it look like before and after?
- When have you knowingly shipped code that was not clean, and how did you pay that debt back?
- How do you handle a review disagreement where both versions are defensible?
What do you think PHP could improve on?
Medium · Technical and Domain Knowledge
Approach
- Show you know the current language before criticising it: PHP 8 added named arguments, match, the nullsafe operator, union types and a JIT, and 8.1 added enums, readonly properties and Fibers. Criticism from PHP 5 memories reads as out of date.
- Pick two or three real gaps with an example each. No native generics: Collection<Order> exists only in PHPDoc for static analysers. Inconsistent standard library: strpos($haystack, $needle) against in_array($needle, $haystack), and array_map(callback, array) against array_filter(array, callback).
- Other defensible points: arrays serve as both lists and maps, which makes intent unclear; loose comparison with == still surprises people even after PHP 8 made 0 == "a" false; and there is no async I/O in core, since Fibers are a primitive that libraries such as ReactPHP or Amp build on.
- Mention the request model: each request bootstraps the application from scratch, which OPcache, preloading (7.4) and long-running servers such as Laravel Octane reduce, at the price of state leaking between requests if you are careless.
- Close each point with how you work around it today (strict_types, PHPStan generics, === by default, typed DTOs instead of associative arrays) so the answer is about engineering judgement, not complaints.
Follow-up
- If you could add one feature to PHP tomorrow, which one and why?
- What changes when a Laravel app runs under Octane instead of PHP-FPM, and what bugs does that introduce?
- How do you get typed collections today without native generics?
Construct two API endpoints using Laravel to list applications and automate order processing, including queue
Medium · Technical and Domain Knowledge
Construct two API endpoints using Laravel to list applications and automate order processing, including queue workers and scheduling.
Approach
- Listing endpoint: GET /api/applications in routes/api.php, a controller index that validates filter and sort parameters, eager loads relations with with() to avoid N+1, returns paginate() or cursorPaginate() results through an ApplicationResource collection, and relies on indexes on the filter and sort columns.
- Order endpoint: POST /api/orders validates through a Form Request, writes the order with status pending inside a transaction, dispatches ProcessOrder::dispatch($order)->afterCommit() so the worker never reads an uncommitted row, and returns 202 Accepted with the order id and status.
- Job design: ProcessOrder implements ShouldQueue with $tries, a backoff, and a failed() method that marks the order failed. Make it idempotent: re-check the status before acting, use ShouldBeUnique or the WithoutOverlapping middleware, and accept an Idempotency-Key header on the POST so a client retry cannot create two orders.
- Workers and scheduling: run php artisan queue:work on Redis or the database driver under Supervisor, and run php artisan queue:restart on deploy. Schedule a sweep (Schedule::job in routes/console.php on Laravel 11, Kernel::schedule earlier) with withoutOverlapping() and onOneServer(), driven by a cron entry calling schedule:run every minute.
- Testing and edge cases: feature tests with postJson and getJson, Queue::fake() with Queue::assertPushed for dispatch, a direct test of the job's handle method, and cases for duplicate submission, an external call timing out mid-job, and orders stuck in pending.
Follow-up
- The worker crashes halfway through processing an order. What state is the order in, and how does it recover?
- How would you report progress or the final result back to the client that called the order endpoint?
- The scheduled sweep runs on three app servers. How do you stop it processing the same orders three times?
p99 jumped on one listing filter while p50 stayed flat
Medium · query planning · composite index · percentiles
After a release that added an owner_user_id filter to the resource listing, p99 rose from 90 ms to 1.9 s while p50 stayed at 40 ms. Traffic and row counts are unchanged. resource carries the index (tenant_id, status, updated_at DESC, resource_id DESC). The new query filters tenant_id and owner_user_id, orders by updated_at DESC, resource_id DESC, and takes 20 rows. On PostgreSQL, explain the shape of the regression, prove it from a query plan, and give the index you would add.
Approach
- Start from the shape. A flat p50 with a moved p99 means a subset of requests changed cost, not all of them, so the first job is naming the subset. Bucket the endpoint's latency by the tenant's row count; the natural hypothesis is that large tenants are a small share of requests and all of the tail.
- Get the plan for the new query on a large tenant with EXPLAIN (ANALYZE, BUFFERS). Expect an index scan over the tenant's range, a filter discarding most of it, then a Sort feeding the Limit, possibly reporting Sort Method: external merge Disk. Read actual rows on the scan node, not estimated.
- Explain why the existing index cannot serve it. A composite B-tree is seekable only as a left prefix, and with no equality predicate on status the scan cannot treat updated_at as an ordering, because rows in the tenant's range are ordered by status first. Everything matching must be read and sorted before LIMIT 20 can apply, so a tenant with 400,000 rows pays 400,000 rows to return 20.
- Add (tenant_id, owner_user_id, updated_at DESC, resource_id DESC). Equality on the first two columns leaves the index ordered by updated_at within that pair, so the plan becomes an index scan that stops after 20 rows with no Sort node. PostgreSQL can scan a B-tree backwards, so the DESC markers matter only if the two sort columns ever disagree in direction; keeping them explicit documents the order the keyset cursor depends on.
Follow-up
- The endpoint paginates with OFFSET. What does page 500 cost with your index, and what does the keyset version cost?
- How would you have caught this before release, given that a 10,000-row seed database produces the same plan shape at an unnoticeable cost?
Suggested practice checklist
The seven days follow the reported material: a Laravel sandbox first, then types and PHP fundamentals, N+1 and performance, queue workers and scheduling, clean code and refactoring, values stories, and a full dry run. Practice-library exercises are attached where they reinforce the same skill.
Day 1 — Confirm the format and set up a Laravel sandbox
- Message your recruiter to ask which stages apply, whether there is a take-home, and whether you will write code live.
- Scaffold a fresh Laravel app with applications, customers and orders tables, migrations and seeders with a few hundred rows.
- List the Laravel features you can explain without notes (Eloquent relations, form requests, queues, scheduler) and mark the gaps.
Deliverable: A sent recruiter message, a running Laravel app with seeded data, and a written list of strong areas and gaps.
Day 2 — Types in PHP and what PHP could improve
- Take a service class in your sandbox and add strict types, parameter and return types, nullable and union types, an enum for order status and readonly properties.
- Write a short spoken answer to what type-hinting is and why it helps, naming what it catches before runtime and what it cannot catch.
- Write a balanced two-point answer on what PHP could improve and what newer versions have already improved.
Deliverable: A typed service class in your sandbox, plus written talking points for the type-hinting answer and the PHP-improvement answer.
Day 3 — N+1 and listing performance
- Build a listing endpoint for applications with their customer and items, and log the number of queries a request makes.
- Fix the N+1 with eager loading, compare query counts before and after, and turn on lazy-loading prevention in development to catch regressions.
Deliverable: A before-and-after query count for the listing endpoint and a written diagnosis sequence for a slow filtered list.
Day 4 — Order-processing endpoint, queue workers and scheduling
- Build the endpoint that accepts an order and dispatches a queued job, with retry limit, backoff, a failed-jobs path and a uniqueness guard so a retry cannot process the same order twice.
- Add a scheduled command that sweeps stuck orders, and run the worker and scheduler locally to prove both work.
Deliverable: A working two-endpoint Laravel app (listing and order processing) with a worker, a scheduled command and a README describing each design decision.
Day 5 — Clean code and refactoring
- Take a long controller method and split it into a form request, an action or service class and an API resource, adding tests around the behavior before and after.
- Write your own definition of clean code with three concrete rules from this refactor, and a sentence on when you would break each.
Deliverable: A before-and-after diff of the refactor with tests, and a written list of your three clean-code rules with exceptions.
Day 6 — Values and experience stories
- Read the company's published values and write one real story per value in situation, action, outcome form.
- Rehearse aloud your background, a mistake and its fix, a favorite value, and a two-values answer where the pair is picked at random from your story bank.
- Prepare answers to going above and beyond for a team member and handling feedback on code, reusing the same stories where honest.
Deliverable: A story bank mapping each value to one or two stories with outcomes, and spoken rehearsals of the four reported behavioral questions.
Day 7 — Full dry run
- Walk through your sandbox app as if defending a take-home: explain each design decision, one thing you would change and one you would not.
- Answer the reported technical questions aloud (N+1, type-hinting, clean code, PHP improvements), then two behavioral ones with a friend picking random values.
- Write the questions you want to ask the team about their stack, code review and how they handle production issues.
Deliverable: A recorded or witnessed dry run, a short list of weak answers to fix, and written questions for the interviewers.
Experience and values stories
Candidates report that this loop mixes technical discussion with questions about your background, a mistake, and the company's stated values, including a question that picks two values at random. For a Software Engineer role that works in PHP and Laravel, the strongest stories are engineering ones: an API you shipped, a production incident you handled, a disagreement in code review. Learn the published values beforehand and match each to a real story with an outcome.
- Describe a time you went above and beyond for a team member.
- How do you handle feedback on your code or process?
Tell us about your background and experience.
Medium · Behavioral and Value-Based Questions
Approach
- Use a present-past-future arc. Open with what you do now, then pick two or three past projects that match this role (a PHP or Laravel service, an API you designed, a database problem you fixed), and close with why this role is the next step.
- Give each project one concrete fact: what you built, your own part in it, and a result you can state (a query count cut, a job that now retries safely, a release that stopped failing). Skip the full chronological CV.
- Be straight about the stack. If you have used Laravel, say which parts (Eloquent, queues, scheduler, form requests). If you have used another framework, name it and say what you would do to become productive in Laravel.
- Keep it short enough that the follow-up questions can go where they want. End on a sentence that invites a question about one project rather than trailing off.
Follow-up
- Which of those projects would you do differently now, and why?
- What was your own contribution versus the team's on the project you mentioned?
- Which part of this role's stack is newest to you, and how would you close the gap?
What is a mistake that you have made and how did you resolve it?
Medium · Behavioral and Value-Based Questions
Approach
- Pick a real mistake with a real cost, ideally an engineering one such as a migration that locked a table, a job that ran twice and double-processed orders, or a lazy-loaded relation that shipped as an N+1. A trivial or disguised-strength answer reads as evasive.
- Structure it as situation, your decision, impact, response. Say how you found out, what you did to contain it first (rollback, pausing the worker, a hotfix), and how you told the people affected.
- Own it in the first person. Describe your reasoning at the time without blaming the process or a colleague, even if the process was weak.
- Finish with what changed permanently: a test, an alert, a review checklist item, a safer deploy step. The lasting fix matters more than the apology.
Follow-up
- How did you find out about it, and how long was it live before you did?
- What exactly changed afterwards so the same mistake could not recur?
- How did you communicate it to your team or the people affected?
What is your favorite Aussie Broadband value and why?
Medium · Behavioral and Value-Based Questions
Approach
- Read the company's published values before the interview and choose one you can back with a real story. Naming a value you picked for sounding good and then having no example is the failure mode.
- Define the value in your own words as observable behavior, for example what it looks like in a code review, an on-call incident or a hand-off, rather than repeating its label.
- Tell one story in situation, action, outcome form, and show the value through what you did and what it cost you or the team, not through adjectives.
- Show you can also see its limits: one sentence on where that value is hard to apply, such as when speed pulls against careful review. That shows judgment instead of recitation.
Follow-up
- Where has living that value been difficult for you?
- Which of the company's values do you find hardest, and why?
- How would that value show up in your daily engineering work here?
Tell us where these two (randomly selected) Aussie Broadband values may have applied to you in a previous role
Medium · Behavioral and Value-Based Questions
Tell us where these two (randomly selected) Aussie Broadband values may have applied to you in a previous role and what was the outcome.
Approach
- Build a story bank before the interview: for every stated company value, one or two real examples from past roles. Because the two values are selected for you, you cannot choose which one you get, so you need coverage of all of them.
- When the two values arrive, restate each in a phrase of practical meaning, then give one story per value. If one honest story genuinely covers both, say so and show where each appears in it.
- The question is whether the values may have applied, so a story where you fell short of a value is acceptable if you say what you learned. Do not stretch a story to fit; a forced match is easy to spot.
- State the outcome in concrete terms, such as a metric you measured, a deadline that held, a customer issue resolved, a relationship repaired. If the outcome was mixed, say so and say what you would do differently.
Follow-up
- Which of the two values was harder to apply there, and why?
- What would you do differently if the same situation came up again?
- How did the other people involved see the outcome?