Software Engineering Cheat Sheet (general, for any interview)
Version: 2 (all technical questions from the other files now included here). Date: 2026-11-13.
This is the main reference. It holds all technical questions from full_stack_interview_questions.md, QinetiQ/qinetiq_interview_prep.md and NATS/nats_interview_prep.md, plus OOP. Company files keep only company-specific content (stories, values, role).
Three parts:
- Part A: Software (OOP, design, errors, data structures, concurrency, APIs, databases, security, JavaScript, front end, TypeScript, testing, Git, architecture, live coding).
- Part B: Hardware and low level (C/C++, memory, embedded, threads, safety).
- Part C: Last-minute quick-fire list.
Format:
Word: simple explanation. Example: real software example. Your link: a line from your own CV/work that you can say.
The answer formula for any "What is X?" question:
1. One-line definition → 2. A real example → 3. Why it matters (benefit) → 4. One trade-off → stop
Example: "Encapsulation means an object hides its data and allows changes only through its methods. For example, a credit card object keeps its balance private and changes it only through charge(), which checks the limit. It protects the data from invalid changes. The trade-off is a bit more code."
PART A: SOFTWARE
A1. Object-Oriented Programming (OOP)
OOP: build software from objects. Each object holds data (fields) and behaviour (methods) together. Class: the template. Object: one instance made from it. Example:
Cardclass → your card object. Constructor: runs when an object is created; sets the starting values.
class Card {
constructor(public owner: string) {} // constructor sets the start values
}
const myCard = new Card("Alwin"); // object = one instance of the class
One example for all of OOP: a checkout with one "Pay" button. Underneath, the payment can be a card, PayPal or Apple Pay. The checkout only calls pay().
Method vs function
Function: a block of code that stands alone. It is not part of a class. Method: a function that belongs to a class or object. It can use the object's data through
this.
function addVat(amount: number) {
// FUNCTION: stands alone, uses only its inputs
return amount * 1.2;
}
class Card {
private balance = 0;
charge(amount: number) {
// METHOD: belongs to Card, uses this.balance
this.balance += addVat(amount);
}
}
addVat(10); // call a function by its name
myCard.charge(10); // call a method ON an object
| Function | Method | |
|---|---|---|
| Belongs to | Nothing (stands alone) | A class / object |
| Called as | addVat(10) | myCard.charge(10) |
Can use object data (this) | No | Yes |
- A static method belongs to the class, not to one object (
Card.isValidNumber("4111...")). It is close to a function, but grouped inside a class. - C++ also says member function for a method. Java has only methods (every function is inside a class).
Constructor and super
Constructor: a special method that runs once, when the object is created (
new). It sets the starting values.super: a way for a child class to reach its parent class.
super(...)in the child's constructor = run the parent's constructor first.super.method()= call the parent's version of a method (often inside an override, to add to it, not replace it).
extends below means "CreditCard is a child of Card" (inheritance, pillar 3).
class Card {
constructor(protected owner: string) {} // parent sets up "owner"
pay(amount: number) {
console.log(`${this.owner} paid ${amount}`);
}
}
class CreditCard extends Card {
private limit: number;
constructor(owner: string, limit: number) {
super(owner); // 1. parent constructor runs first: sets owner
this.limit = limit; // 2. then the child sets its own data
}
pay(amount: number) {
if (amount > this.limit) throw new Error("Over limit"); // child adds a check
super.pay(amount); // then reuses the parent's pay()
}
}
const card = new CreditCard("Alwin", 1000); // constructors run: Card first, then CreditCard
card.pay(50); // CreditCard.pay → check → Card.pay
this.pay() or super.pay()? Second example, with no override:
class Card {
pay(amount: number) {
console.log(`Paid ${amount}`);
}
}
class DebitCard extends Card {
// no pay() here: DebitCard inherits Card's pay()
payWithFee(amount: number) {
this.pay(amount + 1); // ✔ finds pay() on the parent through inheritance
}
}
new DebitCard().payWithFee(50); // "Paid 51"
| Call | Meaning | When to use |
|---|---|---|
this.pay() | "Use this object's pay()": the child's version if it has one, if not the parent's | Normal choice, anywhere in the parent or child |
super.pay() | "Skip the child, use the parent's version" | Only inside a child class, mainly in an override that wants to add to the parent's version |
- No override →
this.pay()already gives the parent's version, sosuper.pay()is not needed. - With an override → inside the child's
pay(), usesuper.pay().this.pay()there would call itself forever (infinite loop). - In the parent class,
superis not used (Card has no parent).thisin the parent means the real object, so if the child overridespay(), the parent'sthis.pay()runs the child's version. That is polymorphism.
Does the parent constructor run automatically?
When you create a child object, the parent constructor must run first, so the parent part of the object is set up. Who starts it depends on one thing: does the child have its own constructor?
class Card {
constructor() {
console.log("Card set up");
}
}
// Case 1: child has NO constructor
class CreditCard extends Card {}
new CreditCard(); // prints "Card set up" → parent constructor ran automatically
// Case 2: child HAS a constructor → you must start the parent yourself
class DebitCard extends Card {
constructor() {
super(); // runs Card's constructor first → "Card set up"
console.log("Debit set up");
}
}
new DebitCard(); // prints "Card set up", then "Debit set up"
// Case 3: child HAS a constructor but forgets super()
class GiftCard extends Card {
constructor() {
console.log("Gift set up"); // TS/JS: ERROR, parent constructor never started
}
}
- Case 1 (no child constructor): the parent constructor runs automatically, in all common languages.
- Case 2 (child constructor +
super()): you start the parent constructor yourself. Always correct. - Case 3 (child constructor, no
super()): the parent constructor does not run automatically in TypeScript, JavaScript (error) and Python (no error, the parent setup is silently skipped). super.method()for a normal method is never automatic.- Simple rule to say: "The parent constructor always runs first. If my child class has a constructor, I call
super(...)myself so it is clear."
Order when you create a child object:
new CreditCard(...) → super(owner): Card constructor runs → CreditCard constructor continues
Rules to know:
- In TS/JS/Java,
super(...)must run before the child usesthis. super.pay()+ extra code = extend the parent behaviour. Nosuper.pay()= replace it fully.- C++ has no
superkeyword. It calls the parent constructor in the initialiser list (the: Card(owner)part after the constructor, which runs before its body), and the parent method by name:CreditCard(std::string owner, int limit) : Card(owner), limit(limit) {} // like super(owner) void pay(int amount) override { Card::pay(amount); } // like super.pay() - Python:
super().__init__(owner). - Destructors (C++) run in the opposite order: child first, then parent.
Constructor overloading: several constructors with different parameters (C++, Java, C#). Example:
Card(owner)andCard(owner, limit). TS/JS allow only one constructor (use optional parameters instead).
The 4 pillars
1. Encapsulation: hide the data, control the access
Encapsulation: keep data private; change it only through methods that apply the rules. Example: a card's balance is private. Nobody can set it directly. Only
charge()can change it, and it checks the limit. The card number is hidden; only the last 4 digits are shown.
class Card {
private balance = 0; // hidden: no direct access
charge(amount: number) {
// the only way in, with a rule
if (amount > 1000) throw new Error("Over limit");
this.balance += amount;
}
}
Why: data stays valid; rules live in one place; sensitive data cannot leak. Your link: "Other services cannot write to my notifications queue directly. They call the API, which validates the request."
2. Abstraction: show what, hide how
Abstraction: give a simple interface; hide the complex details. The caller knows what it does, not how. Example: the checkout calls
pay(50). How it pays is hidden insidepay(): check the card details, contact the bank, wait for approval, record the payment. The checkout never sees these steps. It only gets back "paid" or "declined".
class CardPayment {
pay(amount: number) {
// the caller only sees this one method
this.checkCardDetails(); // ┐
this.askBankToApprove(amount); // │ HOW it pays: hidden inside
this.recordPayment(amount); // ┘
}
}
checkout.pay(50); // caller knows WHAT (pay £50), not HOW
Same idea in real life: you turn the steering wheel; you do not move the steering rack and wheels yourself.
Encapsulation vs abstraction: encapsulation hides the data; abstraction hides the complexity. Your link: "The UI calls one notifications endpoint. It does not know about the template, the queue or the email service."
3. Inheritance: a child inherits the parent's properties and methods
Inheritance: a child class inherits the parent's properties and methods. It can add its own extra properties and methods, and change (override) inherited methods. An "is-a" relation. Example: a credit card is a card; a debit card is a card. Shared steps (check expiry) are written once in
Card.
A child can do 3 things with what it inherits:
class Card {
owner = "";
checkExpiry() {
/* shared by all cards */
}
pay(amount: number) {
console.log(`Paid ${amount}`);
}
}
class CreditCard extends Card {
// 1. INHERIT: owner, checkExpiry() and pay() come for free, nothing to write
// 2. ADD: something only credit cards have
limit = 1000;
payInInstalments(amount: number, months: number) {
/* credit cards only */
}
// 3. CHANGE (override): its own version of a parent method
pay(amount: number) {
if (amount > this.limit) throw new Error("Over limit"); // extra rule
super.pay(amount); // then the parent's pay()
}
}
class DebitCard extends Card {} // only INHERITS: uses Card's pay() as it is
const credit = new CreditCard();
credit.checkExpiry(); // inherited from Card
credit.payInInstalments(300, 3); // added in CreditCard
credit.pay(50); // changed in CreditCard (limit check first)
new DebitCard().pay(50); // Card's original pay()
| What the child does | Example | |
|---|---|---|
| Inherit | Uses the parent's property/method as it is | checkExpiry(), owner |
| Add | New property/method the parent does not have | limit, payInInstalments() |
| Change | Overrides a parent method with its own version | pay() with a limit check |
Risk: a change in the parent affects every child. Prefer composition (an object has other objects and uses them, "has-a"; see "Composition vs inheritance" below) unless it is a true "is-a".
4. Polymorphism: same call, different behaviour
Polymorphism: one method call does different things, depending on the real object type. Example: the same
pay()call charges a card for one customer and opens PayPal for another. Adding Google Pay is one new class; the checkout does not change.
// 1. The parent: every payment method MUST have pay()
abstract class PaymentMethod {
abstract pay(amount: number): void; // no body here: each child writes its own
}
// 2. Children: same method name, different behaviour
class CardPayment extends PaymentMethod {
pay(amount: number) {
console.log(`Charged £${amount} to card`);
}
}
class PayPalPayment extends PaymentMethod {
pay(amount: number) {
console.log(`Sent to PayPal to pay £${amount}`);
}
}
class ApplePayPayment extends PaymentMethod {
pay(amount: number) {
console.log(`Asked phone to approve £${amount}`);
}
}
// 3. The checkout only knows "PaymentMethod", not which one it is
function checkout(method: PaymentMethod) {
method.pay(50); // same call, different behaviour
}
checkout(new CardPayment()); // "Charged £50 to card"
checkout(new PayPalPayment()); // "Sent to PayPal to pay £50"
checkout(new ApplePayPayment()); // "Asked phone to approve £50"
How it works:
checkout(method) → method.pay(50) → at run time, the program looks at the REAL object
→ CardPayment? run its pay() PayPalPayment? run its pay() ApplePayPayment? run its pay()
checkout()was written once. It never checksif (card) … else if (paypal) ….- Add Google Pay: write one new class
GooglePayPayment extends PaymentMethod.checkout()does not change. - Without polymorphism you would need a long
if/elseorswitchon the type, and you would edit it every time a new payment type is added. abstract= the parent cannot be created on its own (new PaymentMethod()is an error) and forces each child to writepay(). Aninterfaceworks the same way here.
extends or implements? Both give polymorphism, but they mean different things:
extends= a real child class (inheritance). It inherits code from the parent. Only one parent.implements= the class follows a contract (an interface). It inherits no code; it only promises to have certain methods. A class can implement many interfaces. An interface is a list of method names (with their inputs and return types) and no code inside: it says what a class must be able to do, not how.
Use
extendswhen the classes are the same kind of thing and share real code. Example:CreditCardandDebitCardare both cards and both need the samecheckExpiry(). Write it once inCard, and bothextends Card.
Use
implementswhen different classes only need to do the same job, but do it in their own way, with no shared code. Example:CardPayment,PayPalPaymentandBankTransfercan all refund, but each refund works differently. So each oneimplements Refundableand writes its ownrefund().
Simple test: do the classes need to share the same code? Yes (both cards use the same
checkExpiry()) →extends. No, they only need the same method name, each with its own code (every payment hasrefund(), but each refund works differently) →implements.
Interface in one example: a capability like "can refund".
interface Refundable { refund(amount: number): void; // only the method's name and shape (its inputs and return type): no body, no code } class CardPayment implements Refundable { refund(amount: number) { console.log(`Refunded £${amount}`); } // the class writes the code }If
CardPaymentforgetsrefund(), the compiler gives an error: it broke its promise.> Note: "interface" also has a general meaning: the public part of anything that others use (e.g. an API is the interface of a service). In OOP questions, it means the keyword above.
Bigger example: one parent class + several capabilities:
// ONE parent class: shared code for every payment
abstract class PaymentMethod {
abstract pay(amount: number): void;
logPayment(amount: number) {
console.log(`Logged £${amount}`);
} // shared code, inherited
}
// CAPABILITIES (interfaces): no code, just promises
interface Refundable {
refund(amount: number): void;
} // "can give money back"
interface Recurring {
payMonthly(amount: number): void;
} // "can take a payment every month"
// Card: extends ONE class + implements TWO interfaces
class CardPayment extends PaymentMethod implements Refundable, Recurring {
pay(amount: number) {
console.log(`Card charged £${amount}`);
}
refund(amount: number) {
console.log(`Card refunded £${amount}`);
}
payMonthly(amount: number) {
console.log(`Card subscription £${amount}/month`);
}
}
// PayPal: can refund, but no monthly payments here
class PayPalPayment extends PaymentMethod implements Refundable {
pay(amount: number) {
console.log(`PayPal paid £${amount}`);
}
refund(amount: number) {
console.log(`PayPal refunded £${amount}`);
}
}
// Gift card: can only pay (no refunds, no subscriptions)
class GiftCardPayment extends PaymentMethod {
pay(amount: number) {
console.log(`Gift card used £${amount}`);
}
}
// Each function asks only for the capability it needs
function checkout(method: PaymentMethod) {
method.pay(50);
method.logPayment(50);
}
function processReturn(method: Refundable) {
method.refund(20);
}
function startSubscription(method: Recurring) {
method.payMonthly(10);
}
checkout(new GiftCardPayment()); // ✔ every payment can pay
processReturn(new PayPalPayment()); // ✔ PayPal is Refundable
startSubscription(new CardPayment()); // ✔ Card is Recurring
processReturn(new GiftCardPayment()); // ✘ compile error: gift card is not Refundable
What this shows:
extends PaymentMethod | implements Refundable, Recurring | |
|---|---|---|
| How many | One | Many |
| Gets code? | ✔ logPayment() comes for free | ✘ the class writes refund() itself |
| Meaning | "Card is a payment method" | "Card can refund and can repeat monthly" |
- The compiler stops mistakes: you cannot refund a gift card, because it does not implement
Refundable. - Functions ask only for what they need (
processReturnwantsRefundable, not "any payment"). This is the I in SOLID (Interface Segregation: small interfaces). - Why not one big parent with
refund()in it? Then gift cards would have arefund()that cannot work. Small interfaces avoid that.
Other languages:
- C++: no
implementskeyword. Use several parents with only pure virtual methods:class CardPayment : public PaymentMethod, public Refundable, public Recurring. - Java: the same as TypeScript (
extendsone class,implementsmany interfaces). - C#: the same idea, written with a colon:
class CardPayment : PaymentMethod, IRefundable, IRecurring. - TypeScript extra: any object with a matching method works, even without
implements("structural typing").
| Type | Decided | How |
|---|---|---|
| Run-time (dynamic) | While running: the program looks at the real object | Overriding (C++ needs virtual) |
| Compile-time (static) | When building: the compiler picks the version from the argument types | Overloading, operator overloading, templates/generics |
Your link: "One template per notification type. The processor does the same 'render and send' for every type."
Access modifiers: public, private, protected
Access modifier: says who can use a field or method. The tool for encapsulation.
| Modifier | Who can use it | Example |
|---|---|---|
public | Anyone | pay() |
private | Only this class | balance, card number |
protected | This class + subclasses | a shared checkExpiry() that subclasses can use and change, but the checkout cannot |
class Card {
public pay() {} // anyone
protected checkExpiry() {} // Card + subclasses
private balance = 0; // Card only
}
class CreditCard extends Card {
test() {
this.pay(); // ✔ public: allowed everywhere
this.checkExpiry(); // ✔ protected: allowed in a subclass
this.balance; // ✘ private: compile error
}
}
const card = new CreditCard();
card.pay(); // ✔ public: outside code can call it
card.checkExpiry(); // ✘ protected: outside code cannot
Can a subclass change a protected method, or only use it? Both. Protected means a subclass can call it and override it with its own version:
class DebitCard extends Card {
protected checkExpiry() {
/* own version for debit cards */
} // ✔ override
}
- The access modifier controls who can see it. It does not stop overriding.
- To stop a subclass from changing a method: Java
final, C++final, C#sealed(C# also needsvirtualto allow overriding at all). TypeScript has nofinal. - In C++, the parent method must be
virtualfor the override to work through a parent pointer (run-time polymorphism). - Private methods: a subclass cannot call them or override them.
Rule to say: "Start with everything private. Open up only what is needed. The public part is a promise to other code."
| Language | Note |
|---|---|
| TypeScript | private is checked only when compiling; in the final JavaScript anyone can still read it. #field is real privacy, even at run time. Default: public |
| C++ | class members private by default; struct public by default. friend can see private members |
| Java | Also package-private: no keyword = visible only inside the same package (folder of classes) |
| Python | No real privacy. A leading _name just means "internal, please do not use" |
Overloading
Overloading: several methods with the same name in the same class, but different inputs (number or type of parameters). The compiler picks the right one from what you pass in. Example:
refund(id)gives a full refund;refund(id, amount)gives a partial refund.
void refund(int id); // full refund
void refund(int id, int amount); // partial refund: same name, extra input
refund(42); // calls the first one
refund(42, 500); // calls the second one
- Decided when the code is compiled (compile-time polymorphism).
- Not the same as overriding (a child replaces a parent's method, see Inheritance).
- TypeScript/JavaScript: no real overloading (TS lets you list several input options, but there is only one function body).
Operator overloading
Operator overloading: the same idea, but for operators (
+,==,<) instead of method names. You give an operator a custom meaning for your own class. Example: add twoMoneyvalues witha + b.
Money operator+(const Money& other) const {
return Money(pence + other.pence); // "+" now adds two Money objects
}
- Good for maths-like types:
Money,Vector,Matrix, strings. - The operator must do what people expect (
+adds; it must not, for example, save to a database). - Supported: C++, C#, Python (
__add__). Not in Java, JavaScript, TypeScript. - Store money in whole pence, not floats (decimal numbers): computers store floats inexactly, so
0.1 + 0.2gives0.30000000000000004.
Abstract classes and abstract methods
Abstract class: a class that is only a base for other classes. You cannot create an object from it directly. It exists to share code and to set rules for its children. Abstract method: a method with no body in the parent. Every child must write its own version.
abstract class PaymentMethod {
constructor(protected owner: string) {} // ✔ can have a constructor
abstract pay(amount: number): void; // abstract: no body, child MUST write it
logPayment(amount: number) {
// normal method: real code, inherited
console.log(`${this.owner} paid £${amount}`);
}
}
class CardPayment extends PaymentMethod {
pay(amount: number) {
console.log("Card charged");
} // ✔ writes the abstract method
}
class BrokenPayment extends PaymentMethod {} // ✘ compile error: pay() is missing
new PaymentMethod("Alwin"); // ✘ compile error: cannot create an abstract class
new CardPayment("Alwin").logPayment(50); // ✔ create the child; it inherits logPayment()
Rules to remember:
- An abstract class can have: normal methods with code, properties, a constructor, and abstract methods.
- An abstract class cannot be created with
new. - A child must write every abstract method, or be abstract itself.
- Why use it: share common code once (
logPayment()) and force each child to fill in the part that differs (pay()). It stops someone creating a "payment" that has no way to pay. - Real-life idea: "Payment method" is a general idea. You never pay with "a payment method"; you pay with a card or PayPal. So the general idea is abstract; the real types are not.
In other languages:
- Java / C#: the same
abstractkeyword. - C++: no
abstractkeyword. A class is abstract if it has at least one pure virtual method:virtual void pay() = 0; - Python: inherit from
ABC(Abstract Base Class) and mark methods with@abstractmethod.
Interface vs abstract class
Interface: a contract; lists methods, no code. A class can implement many. Abstract class: a parent you cannot create directly; can have some code. A class extends only one.
| Interface | Abstract class | |
|---|---|---|
| Code inside? | No | Yes |
| How many? | Many | One |
| Use when | Unrelated classes share a capability (Refundable) | Related classes share code (Card with checkExpiry()) |
C++ has no interface keyword: use a class with only pure virtual functions (virtual void pay() = 0;).
Composition vs inheritance
Composition: a class keeps another object in one of its fields and calls that object's methods to do part of the work. "Has-a" = "has one of these in a field". Example: a
PaymentServicehas a fraud checker in a field. Before each payment, it asks the fraud checker "is this safe?". A payment service is not a kind of fraud checker; it uses one.
It does not mean a class written inside another class (that is a "nested class", a different thing). Both classes are written separately; one just holds the other.
interface FraudChecker {
// an interface: any fraud checker must have check()
check(amount: number): boolean;
}
class SimpleFraudChecker implements FraudChecker {
check(amount: number) {
return amount < 1000;
} // a real checker
}
class PaymentService {
private fraud: FraudChecker; // 1. a private FIELD that holds a fraud checker
constructor(fraud: FraudChecker) {
// 2. the checker is created OUTSIDE and passed in
this.fraud = fraud; // 3. store it in the field
}
pay(amount: number) {
if (!this.fraud.check(amount)) throw new Error("Blocked"); // 4. USE it: composition
console.log(`Paid £${amount}`);
}
}
const service = new PaymentService(new SimpleFraudChecker()); // real one in the app
const testService = new PaymentService({ check: () => true }); // fake one in a test
- Why an interface?
PaymentServiceonly needs "something withcheck()". So you can pass in the real checker, a stricter one, or a fake one in tests, with no change toPaymentService. - Why pass it into the constructor? So the outside decides which checker to use (this is dependency injection). If
PaymentServicedidnew SimpleFraudChecker()itself, you could never swap it.
TypeScript shortcut (parameter property): writing
privatein front of a constructor parameter creates the private field and stores the value for you. These two are the same:// long form (as above) private fraud: FraudChecker; constructor(fraud: FraudChecker) { this.fraud = fraud; } // short form: same result, one line constructor(private fraud: FraudChecker) {}Other languages (Java, C#, C++) have no shortcut: you write the field and the assignment yourself.
Why prefer it: swap parts easily, less coupling, no deep class trees. Rule: inheritance for is-a (credit card is a card); composition for has-a (service has a fraud checker).
What else can a class have?
Short list of class features you may be asked about:
class Card {
static count = 0; // STATIC: belongs to the class, shared by all cards
readonly number: string; // READONLY: set once, never changed
#pin = "1234"; // PRIVATE (#): real hidden field
private _limit = 1000;
constructor(number: string) {
// CONSTRUCTOR: runs on "new"
this.number = number;
Card.count++; // static used through the class name
}
get limit() {
return this._limit;
} // GETTER: read it like a property: card.limit
set limit(value: number) {
// SETTER: write it like a property, with a rule
if (value < 0) throw new Error("Limit cannot be negative");
this._limit = value;
}
static isValid(number: string) {
// STATIC METHOD: call on the class: Card.isValid(...)
return number.length === 16;
}
describe() {
return "Card";
} // normal method
}
class CreditCard extends Card {
override describe() {
return "Credit card";
} // OVERRIDE keyword: "this replaces a parent method"
}
class Box<T> {
// GENERIC class: works with any type T
constructor(public item: T) {}
}
const cardBox = new Box<Card>(new Card("4111111111111111"));
| Feature | Simple meaning | Example |
|---|---|---|
static property / method | Belongs to the class, not one object | Card.count, Card.isValid() |
readonly (C++ const, Java final field) | Set once, never changed | number |
Getter / setter (get / set) | Read/write like a property, but with code and rules | card.limit = 2000 runs the check |
override keyword | Marks a method that replaces a parent's. The compiler checks the parent really has it | override describe() |
final (Java, C++) / sealed (C#) | Stops overriding or stops other classes extending this one. TypeScript has no final | final class Card |
| Private constructor | Nobody outside can use new; the class controls creation (Singleton = only one object ever; factory = a method that builds objects for you) | Card.create(...) |
| Generic class | One class that works with many types; T is a placeholder for the real type | Box<Card>, Box<string> |
| Nested / inner class | A class inside another class (Java, C#, C++) | Order.Item |
| Destructor (C++) | Runs when the object is destroyed; frees resources it holds, e.g. memory or a file (RAII, see B2) | ~Card() |
| Friend (C++) | Lets a named class/function see private members | friend class Auditor; |
Other OOP terms
| Term | Meaning |
|---|---|
static | Belongs to the class, not one object |
this / super | The current object / the parent class (see "Constructor and super") |
| Getter / setter | Controlled read / write: a method runs, so it can check rules (e.g. limit cannot be negative) |
| Immutable | Cannot change after creation (a past transaction) |
| Coupling | How much classes depend on each other. Want low: a change in one class should not break many others |
| Cohesion | How focused a class is on one job. Want high: everything in it belongs together |
| Multiple inheritance | Two parents (C++ allows it) |
| Diamond problem | B and C both inherit from A; D inherits from B and C. If both changed a method from A, which one does D get? (C++ fixes it with virtual inheritance) |
| Virtual function (C++) | A method a child can override; the right version is picked at run time using the vtable (a hidden table, per class, that points to the correct method) |
| Virtual destructor (C++) | Needed in a base class. Without it, delete through a parent pointer runs only the parent's clean-up and the child's part leaks |
| Dependency injection | Pass the things a class needs (database, gateway) in through the constructor, instead of creating them inside. Then tests can pass in fakes |
OOP vs functional
- OOP: data + behaviour in objects. Good for things with state (data that changes over time, like a balance).
- Functional: pure functions (same input → always same output, no side effects), no shared changing state. Good for data transformations and testing.
- Your link: "React moved from class components to functions with hooks. I use both styles."
30-second answer: "Explain the 4 pillars"
"Take a checkout with one Pay button. Encapsulation: a card keeps its balance private and only
charge()can change it, with the limit check. Abstraction: the checkout only callspay(); how it pays (check the card, ask the bank, record it) is hidden inside. Inheritance: credit and debit cards extend aCardclass, so shared checks are written once. Polymorphism: the samepay()call charges a card or opens PayPal, so adding Google Pay is one new class with no change to the checkout. For has-a relations, like a fraud checker, I prefer composition."
30-second answer: "public vs private vs protected?"
"Public: anyone can use it, like
pay(). Private: only that class, like the balance. Protected: the class and its subclasses, like a shared expiry check that a subclass can use or override, but outside code cannot call. I start private and open up only what is needed. In TypeScript,privateis compile-time only;#fieldis real privacy. In C++, class members are private by default."
A2. SOLID and design patterns
SOLID (5 design rules)
| Letter | Rule | Simple meaning | Payment example |
|---|---|---|---|
| S | Single Responsibility | One class, one reason to change | FraudChecker only checks fraud; ReceiptMailer only sends receipts |
| O | Open/Closed | Open to extension, closed to change | Add ApplePay as a new class; do not edit the checkout |
| L | Liskov Substitution | A child must work anywhere the parent works, with no surprises | Any PaymentMethod can go into the checkout; a child must not throw "not supported" for pay() |
| I | Interface Segregation | Many small interfaces, not one big one | Refundable separate from PaymentMethod (gift cards cannot refund) |
| D | Dependency Inversion | Depend on interfaces, not concrete classes ("concrete" = a real, specific class) | PaymentService depends on a CardGateway interface, not on StripeGateway, so you can swap the provider |
Your link: "My notifications design follows S and O: each service has one job, and a new notification type is a new template, not a code change."
Common design patterns
| Pattern | Simple meaning | Example |
|---|---|---|
| Singleton | Only one instance exists | One database connection pool. Risk: hidden global state, hard to test |
| Factory | A method creates the correct object type for you | PaymentFactory.create("paypal") returns a PayPal object |
| Strategy | Swap an algorithm (a way of doing a task) at run time | Different fee rules per card type, chosen when the payment runs |
| Observer / pub-sub | Objects subscribe ("tell me when X happens"); when the event is published, all subscribers react | "Payment succeeded" → receipt service, loyalty points, analytics all react. Your link: your notifications and Agenda queue |
| Adapter | Wrap one interface to look like another | Wrap an old bank API so it fits your CardGateway interface. Your link: legacy modernisation |
| Decorator | Add behaviour by wrapping an object, without changing its class | Wrap a gateway so every call is also logged or retried |
| Repository | Hide database access behind a class, so the rest of the code never writes SQL directly | TransactionRepository.findById() |
| Middleware / chain of responsibility | A request passes through a chain of steps | Your link: Express middleware: logger → auth → validate → handler |
A3. Errors and exceptions
Exception: throw → jumps up the call stack → caught by try/catch higher up
Return value: function returns an error/result → the caller checks it at once
- Throw for unexpected problems the code nearby cannot recover from. Example: payment database connection lost; config missing at start-up.
- Do not throw for expected results. Example: card declined, insufficient funds, invalid card number (400), no permission (403). These are normal business results → return a result.
| Positives | Negatives |
|---|---|
| Cannot be silently ignored (uncaught → program stops) | Hidden control flow: any call can jump out, and the code does not show it |
Main code stays clean (errors handled in one catch) | Slow, timing not predictable (bad for real-time) |
| Travels up many levels with no extra code | Bigger code size (matters on small embedded chips) |
| Carries a message + stack trace (the list of calls that led to the error) | Resource leaks if careless: a file stays open. Fix: RAII (C++) or finally |
- Real-time / safety coding rules (MISRA C++, JSF AV C++) often ban exceptions → use return codes,
std::optional("a value or nothing") orstd::expected("a value or an error"). - Custom error classes:
class CardDeclinedError extends Error→ the error middleware maps it to the correct status code. - Your link: "In Express, a failed call to another microservice throws → error middleware catches it → 500 and a log. Bad input is expected → validate → 400, no throw."
A4. Data structures and Big O
| Structure | Good for | Payment example |
|---|---|---|
| Array / list | Ordered items; get item N instantly, O(1) | List of transactions |
Hash map (JS Map / object) | Key → value; find by key instantly, O(1) | Card ID → card details |
| Set | Unique items, fast "contains?" | Seen idempotency keys |
| Queue (FIFO = first in, first out) | Process in arrival order | Payment jobs; your Agenda queue |
| Stack (LIFO = last in, first out) | Undo, the call stack | Undo steps in a form |
| Tree | Sorted data, parent → child levels | Database indexes (a B-tree keeps keys sorted for fast search) |
| Graph | Items (nodes) and the links between them (edges) | Fraud: cards linked by the same device |
Big O: how run time grows when the input grows (n = number of items). O(1) same time for any size (hash lookup) · O(log n) halve the work each step (binary search) · O(n) one loop over all items · O(n log n) a good sort · O(n²) a loop inside a loop (1,000 items → 1,000,000 steps).
A5. Concurrency and threads (software view)
Concurrency: many tasks in progress at once, taking turns. Example: one chef cooks 3 dishes, switching between them. Parallelism: many tasks at the same instant on many cores. Example: 3 chefs, 3 dishes.
Process: a running program with its own memory. Thread: a path of execution inside a process; threads share memory.
Race condition: two threads change shared data at the same time. Example: two payment requests read balance £100, both charge £80, both write £20. You lost £80. Fix: a lock (mutex: one thread at a time), an atomic operation (done in one step), a database transaction, or optimistic locking (see A7).
Deadlock: two threads wait for each other forever. Fix: take locks in the same order.
JavaScript: your code runs on one thread (the event loop). Slow I/O (input/output: database, network, files) is handed off, so your thread does not wait:
- Front end (browser): the browser does the slow work (e.g.
fetch). CPU-heavy work freezes the page. - Back end (Node): Node does the slow work, so one thread serves many requests. CPU-heavy work blocks every user.
Concurrency (1 core): A ▮▮ B ▮▮ A ▮▮ C ▮▮ B ▮▮ ← tasks take turns
Parallelism (3 cores): A ▮▮▮▮▮▮
B ▮▮▮▮▮▮ ← tasks run together
C ▮▮▮▮▮▮
| Process | Thread | |
|---|---|---|
| Memory | Own | Shared with other threads |
| Cost to start | High | Low |
| Talk to others | Messages, pipes, sockets (channels between programs) | Shared variables (needs locks) |
| One crashes | Others keep running | Can crash the whole process |
| Single-threaded | Multithreaded | |
|---|---|---|
| Good | Simple, no shared-memory races, easy to debug | Uses all cores; one slow task does not stop others |
| Bad | One slow task blocks all; one core only | Races, deadlocks; harder to test |
Thread-safe: works correctly when many threads use it at once. Semaphore: a counter that limits users of a resource. Example: max 10 connections to the card gateway.
JavaScript in more detail:
Your JS code (1 thread: the event loop)
↓ slow I/O (database, network, files)
Browser (front end) or Node (back end) does the slow work
↓ done
Callback → queue → event loop runs it when the stack is empty
- I/O-heavy (APIs, DB): Node is great; one thread serves thousands of requests because it does not wait.
- CPU-heavy (big reports, encryption in a loop, huge JSON): blocks the event loop → every user waits.
app.get("/statement", (req, res) => {
const pdf = buildHugeStatementPdf(); // 5 s of CPU → ALL users wait 5 s
res.send(pdf);
});
How to make JS parallel:
| Option | What it is | When |
|---|---|---|
| Worker threads (Node) | Real threads, own event loop + memory, talk by messages | CPU-heavy work |
| Web Workers (browser) | Background thread, no DOM access | Heavy work that would freeze the UI |
| Cluster (Node) | Starts one copy of your server per CPU core; they share one port (e.g. 3000) | Use all cores for a server |
| Child processes | Run another program | Scripts, tools |
| More containers | Run more copies of the service (horizontal scaling) in Kubernetes | Production |
| SharedArrayBuffer + Atomics | Memory that workers share; Atomics makes changes safe | Rare, fast data sharing |
const worker = new Worker("./heavy.js"); // heavy work runs on another thread
worker.on("message", (result) => use(result)); // main thread stays free meanwhile
async/awaitdoes not make code parallel; it only stops the thread from waiting.- Workers copy data in messages (they do not share variables) → fewer races than C/C++ threads.
SharedArrayBufferbrings races back → useAtomics. - Many small tasks → a worker pool: a fixed group of ready workers that take tasks from a queue, so you do not start a new thread each time (example library:
piscina).
30-second answer: "Is JS single-threaded? How do you make it multithreaded?"
"My JS code runs on one thread with an event loop. Node passes slow I/O to the OS and a small thread pool, so one thread serves many requests. CPU-heavy work blocks every request, so I move it to worker threads or a worker pool. To use all cores for a server, I use the cluster module or more containers in Kubernetes. Async/await does not make code parallel; it only stops the thread from waiting."
30-second answer: "Concurrency vs multithreading?"
"Concurrency means many tasks are in progress at once; they can take turns on one core, like Node's event loop. Multithreading is one way to do it: threads share memory and can run in parallel on many cores. It is faster for CPU work, but shared memory brings races and deadlocks, so I protect shared data with locks or atomics, or avoid sharing and pass messages."
Distributed concurrency (your world):
| Problem | Fix |
|---|---|
| Two updates overwrite each other | Transactions, or optimistic locking (each record has a version number; save only if it did not change) |
| A retry creates a second charge | Idempotency key (a unique ID per attempt; the server ignores repeats, see A6) |
| Too many requests at once | A queue |
| Two workers take the same job | Job locking (Agenda locks jobs in MongoDB) |
Your link: "Two copies of a service can update the same record. I use transactions, optimistic locking, idempotent APIs, and a queue. Agenda locks each job in MongoDB so two workers do not take the same one."
A6. APIs
REST: each resource has a URL; HTTP methods act on it. Example:
GET /payments/123.
| Method | Action | Idempotent? |
|---|---|---|
| GET | Read | Yes |
| POST | Create | No |
| PUT | Replace | Yes |
| PATCH | Change part | Not always |
| DELETE | Remove | Yes |
Idempotent: sending the same request many times has the same effect as once. Example: a customer clicks "Pay £50" and the network drops. The app retries. Without idempotency → charged twice. With an idempotency key (a unique ID per payment attempt), the server sees the same key and returns the first result → charged once.
POST /payments Idempotency-Key: 7f3a-... → charge £50, store key + result POST /payments Idempotency-Key: 7f3a-... → key seen → return stored result, no new chargeYour link: "My notifications jobs use the same idea: a unique ID per notification, checked before sending, so a retry does not send two emails."
| Status | Meaning | Payment example |
|---|---|---|
| 200 / 201 | OK / created | Payment created |
| 202 | Accepted: request received, the work happens later | Payment queued (your link: notifications endpoint) |
| 400 | Bad request | Invalid card number format |
| 401 / 403 | Not logged in / no permission | Not your account |
| 404 | Not found | Unknown transaction |
| 409 | Conflict with the current state | Duplicate payment |
| 422 | Valid format, business rule failed | Card expired |
| 429 | Too many requests | Rate limit on login attempts |
| 500 / 503 | Server error / unavailable | Bank gateway down |
- REST vs GraphQL: REST has many URLs and the server decides what each one returns. GraphQL has one URL and the client asks for exactly the fields it needs (less extra data, but harder to cache).
- Middleware: small functions that run in order on every request before the main code. request → logger → auth (logged in?) → validate (input OK?) → handler → response.
- API versioning: put a version in the URL (
/v1/payments). When you make a breaking change, add/v2, so old apps on/v1keep working. - Pagination: return a big list in pages, never 1 million transactions at once.
limit= how many per page. Cursor = a bookmark (usually the ID of the last item you got); the next request says "give me the next 50 after this one". Better than page numbers when new items keep arriving.
A7. Databases
| SQL (PostgreSQL) | NoSQL (MongoDB) | |
|---|---|---|
| Shape | Tables with a fixed schema (rules for columns) | Flexible JSON-like documents |
| Relations | Strong: joins combine tables | Weak: data is often copied into each document |
| Best for | Payments, accounts, ledgers | Changing data, logs, your Agenda jobs |
ACID: Atomicity (all or nothing), Consistency (rules always hold), Isolation (parallel transactions do not mix), Durability (saved data survives a crash). Example: a transfer: take £100 from account A and add £100 to account B. If step 2 fails, step 1 is undone.
- Index: an extra sorted lookup structure that makes searches fast (like the index at the back of a book). Cost: writes are a bit slower and it uses space. Example: index on
card_id. - N+1 problem: 1 query to get a list, then 1 more query for each item (100 items = 101 queries). Fix: one join, or load all related rows in one batch query.
- SQL injection: an attacker types SQL into a form field, and your code runs it. Fix: parameterised queries (send the SQL and the user values separately, so values are never run as code).
- Optimistic locking: each row has a version number. Read version 3 → save only "if version is still 3". If someone else saved first, the save fails and you retry. Good when clashes are rare.
- Normalisation: store each fact once; link tables by ID. Example: the customer address is in one
customerstable, not copied into every order.
A8. Security
| Topic | One line |
|---|---|
| Authentication vs authorisation | Who are you (log in) vs what are you allowed to do (permissions) |
| Session vs JWT | Session: the server stores the login and the browser keeps only an ID. JWT: a signed token that holds the user info; the server just checks the signature. A JWT is hard to cancel before it expires → keep expiry short |
| OAuth | Let an app use your account elsewhere without sharing your password ("Log in with Google") |
| XSS (cross-site scripting) | Attacker puts a script into your page (e.g. in a comment) and it runs in other users' browsers. Fix: escape output (show <script> as text), CSP (a header that limits which scripts can run). React escapes by default |
| CSRF (cross-site request forgery) | A bad site makes your browser send a request to your bank, and the browser adds your login cookie. Fix: SameSite cookies (not sent from other sites), CSRF tokens (a secret value the bad site cannot know) |
| CORS | Browser rule: a page on site A cannot read responses from site B unless B allows it with a header (Access-Control-Allow-Origin) |
| Passwords | Never store plain text. Use a slow hash (one-way scramble) + salt (random extra data per user, so equal passwords look different). Tools: bcrypt, Argon2 |
| Secrets | API keys, passwords: keep in a secret store (e.g. AWS Secrets Manager, Kubernetes Secret), never in Git or front-end code |
| Least privilege | Each user/service gets only the access it needs |
| PCI DSS | Security rules for anyone who handles card data. Never store the full card number or CVV yourself; use tokenisation |
Tokenisation: replace the real card number with a random token. Only the payment provider can map it back.
TLS
TLS: encrypts data between two computers. HTTPS = HTTP + TLS. Gives confidentiality, integrity, authentication.
Client hello → server sends certificate → client checks it (trusted CA, name, expiry)
→ both agree a session key → all data encrypted with that key
- Asymmetric keys (a public + private pair; slow) are used only in the handshake to agree a key safely. Then a symmetric key (one shared key; fast) encrypts the data.
- Certificate: a server's digital ID card, signed by a trusted CA (Certificate Authority).
- mTLS (mutual TLS): the client shows a certificate too, so both sides prove who they are. Common between microservices.
- Use TLS 1.2/1.3 only (older versions are broken). Never turn off certificate checks, even in test code. Monitor certificate expiry: an expired certificate stops a live system.
A9. JavaScript core
| Question | Short answer | Example |
|---|---|---|
let vs const vs var | let: visible only inside its { } block, can change. const: same, but cannot be reassigned. var: visible in the whole function, old and error-prone; do not use | const total = calcTotal(basket) |
== vs === | == converts types first ("5" == 5 is true). === checks value and type. Always use === | if (status === "approved") |
| Truthy / falsy | Falsy: false, 0, "", null, undefined, NaN. Everything else is truthy | amount || 10 replaces a real 0 with 10 → use amount ?? 10 |
null vs undefined | undefined: never set. null: set to "no value" on purpose | card.expiry === undefined (missing field) |
| Scope | Where a variable is visible | A let in an if block is not visible outside |
| Hoisting | JS registers declarations before running the code. Using a var too early gives undefined; using let/const too early throws an error | console.log(x); let x = 1; → error |
this | The object that calls the function. An arrow function (() => {}) has no this of its own: it uses the this of the code around it | card.charge() → this is cardInside charge(): setTimeout(() => this.save(), 100) → this is still card. With function () {} instead, this is not card any more (the timer calls it, not card) |
| Prototype | Each JS object has a hidden link to another object (its prototype). If a property is not found, JS looks there, then the next one (the "chain"). class is nicer syntax on top of this | CreditCard.prototype.charge |
| Spread / destructuring | Spread (...) copies items into a new array/object. Destructuring takes values out into variables | { ...card, limit: 2000 }, const { last4 } = card |
map / filter / reduce | Transform / select / combine an array | payments.filter(p => p.ok).reduce((s, p) => s + p.amount, 0) |
| Shallow vs deep copy | Shallow (spread) copies only the top level; objects inside are still shared with the original. Deep copies everything. Use structuredClone(x) for deep | Change copy.address.city after a spread → original changes too |
Pass by value vs pass by reference: what a function gets when you pass it a variable.
- By value: the function gets a copy. Changes inside do not affect the original.
- By reference: the function gets access to the original. Changes inside do affect it.
JavaScript: primitives (number, string, boolean) are passed by value. Objects and arrays pass a copy of the reference, so changes to their contents affect the original.
function addFee(amount) { amount += 1; } // gets a COPY of the number let price = 50; addFee(price); // price is still 50 function addFeeToOrder(order) { order.total += 1; } // gets a reference to the SAME object const order = { total: 50 }; addFeeToOrder(order); // order.total is now 51Strictly, JS is "pass by sharing": you can change the object's contents, but
order = {}inside the function does not replace the caller's object. Why it matters: in React, change state by making a new object ({ ...order, total: 51 }), not by changing the old one. React compares references; if the reference is the same, it may not re-render.
Closure: a function that remembers the variables from where it was created.
function makeRateLimiter(max) { let count = 0; // remembered by the inner function return () => ++count <= max; // ++count: add 1 FIRST, then compare. True while under the limit } const allowPayment = makeRateLimiter(3); allowPayment(); // true, true, true, then false
Event loop: JS runs one task at a time. Slow work (network, timers) goes to the browser/Node. When it finishes, its callback waits in a queue. The event loop runs it when the call stack is empty.
Call stack runs code → slow work goes to browser/Node → callback into a queue → event loop moves it to the stack when emptyMicrotask vs macrotask: two waiting queues. Promise callbacks (microtasks) all run before the next timer or I/O callback (
setTimeout, macrotasks).setTimeout(() => console.log("timer"), 0); Promise.resolve().then(() => console.log("promise")); console.log("sync"); // sync → promise → timer
Promise: an object for a future value. States: pending, fulfilled, rejected. async/await: cleaner Promise code.
async function getPayment(id) { const res = await fetch(`/api/payments/${id}`); // waits, but does not block the thread return res.json(); }
Promise.all: run in parallel; wait for all; if one fails, the whole thing fails at once.Promise.allSettled: wait for all; give back each result (success or failure). Good when some may fail.Promise.race: give back the first one to finish. Example: a request vs a 5-second timeout.
Debounce: wait until the user stops typing before acting. Example: card-number lookup after 300 ms of no typing. Throttle: run at most once per time window. Example: a scroll handler once per 100 ms.
A10. Front end: React, Next.js and the web
How a page gets to the screen
Browser asks for URL → server sends HTML → browser builds the DOM → loads CSS (CSSOM)
→ runs JavaScript → layout → paint → page on screen
DOM: the tree of HTML elements in the browser. JavaScript changes the page by changing the DOM. CSSOM: the same kind of tree, but for the CSS styles. Render-blocking: CSS and some scripts stop the page from showing until they load. Fix:
defer(load now, run after the HTML is read) orasync(run as soon as loaded) on scripts.
React
React: a library to build UIs from components. You describe the UI for the current data; React updates the screen when the data changes. Virtual DOM + reconciliation: React builds a new copy of the UI in memory, compares it with the old copy (diffing), and updates only the changed parts of the real DOM. Example: one transaction status in a list of 100 changes → React updates 1 row.
| Topic | Short answer |
|---|---|
| Props vs state | Props: data passed in from the parent, read-only. State: data the component owns; when it changes, React re-renders (draws it again) |
| What causes a re-render? | State change, props change, parent re-renders, context value changes |
key in lists | Stable ID so React knows which item changed. Use transaction.id, not the index |
| Controlled vs uncontrolled input | Controlled: React state holds the input value (easy to validate). Uncontrolled: the browser holds it; you read it when needed with a ref |
useState | A value that can change |
useEffect | Side effects (work outside drawing the UI: fetch, timers) after render. Return a clean-up function that runs when the component leaves |
useMemo / useCallback / React.memo | Remember (cache) a calculated value / a function / a component's output, so React does not redo work when nothing changed. Use only for real performance problems |
useRef | Keeps a value that does not cause a re-render when it changes, or points to a real DOM element (e.g. to focus an input) |
useContext | Read shared data without passing props through many levels |
| Context vs Redux/Zustand | Context: share data that changes rarely (theme, logged-in user). A state library: big, often-changing shared state, with less re-rendering |
| Server state | Data that comes from an API. Libraries like React Query / SWR handle caching, loading spinners, errors and refetching for you |
| Custom hook | Your own function starting with use that bundles hooks so many components can reuse them. Example: usePaymentStatus(id) |
| Error boundary | Catches render errors in children and shows a fallback, so the whole app does not crash |
| Class vs function components | Old: classes with lifecycle methods (componentDidMount etc.). Now: functions + hooks (simpler, less code) |
useEffect(() => {
const id = setInterval(refreshPaymentStatus, 5000);
return () => clearInterval(id); // clean-up: without it, timers pile up (memory leak)
}, []);
CSR, SSR, SSG, ISR
| CSR | SSR | SSG | ISR | |
|---|---|---|---|---|
| HTML built | In the browser | On the server, each request | At build time | At build, then refreshed |
| First load | Slow | Fast | Fastest | Fastest |
| SEO | Weak | Good | Good | Good |
| Freshness | Live | Live | Old until rebuild | Nearly live |
| Example | Internal admin dashboard | Account / statement page | Help pages | Product / pricing pages |
CSR / SSR / SSG / ISR: Client-Side Rendering (browser builds the page) / Server-Side Rendering (server builds it per request) / Static Site Generation (built once when you build the app) / Incremental Static Regeneration (built once, then rebuilt in the background every N seconds). SEO = how well search engines (Google) can read and rank the page (see SEO below). Hydration: after SSR, React runs in the browser and attaches click handlers etc. to the server HTML, so the page becomes interactive. Hydration error: server HTML and first browser render differ. Example:
Date.now()orwindowused during render.
Next.js
Next.js: a React framework with routing, server rendering, API routes, bundling and optimisation built in.
Why Next.js:
- Choose SSR, SSG, ISR or CSR per page.
- File-based routing: a file in the
app/folder becomes a URL (app/payments/page.tsx→/payments). - Better first load and SEO.
- Built-in optimisation:
next/image(resizes images), code splitting per route (each page loads only its own code), font loading. - Back end in the same project: route handlers (API endpoints) and server actions (server functions called from forms).
React Server Components (RSC): run only on the server, can read the database, send no JS to the browser. Add
"use client"only where you need state, effects or events. Example: the statement list is a server component; the "Dispute transaction" button is a client component. Server actions: server functions called from a form, with no hand-written API route. Next.js middleware: code that runs before a page or API request is handled. Example: redirect users who are not logged in.
Trade-offs: more complex than plain React; needs a server for SSR; you must think about what runs where.
Compilation: front end vs back end vs C++
(For JIT vs AOT advantages, disadvantages and uses, see A16.)
Front end: TS/TSX → transpile (tsc/Babel/SWC/esbuild) → bundle (webpack/Vite/Turbopack)
→ tree shake → minify → code split → small files over the network → browser JIT
Node: TS → transpile → Node runs it (V8 JIT). Bundle size matters much less
C++: source → compiler → machine code for one CPU (ahead of time) → linker → executable
Transpile: convert to another language at the same level (TS → JS). Types are removed. Bundler: joins many code files (modules) and libraries into a few files, so the browser makes fewer downloads. Tree shaking: removes code you import but never use. Minify: removes spaces and comments and shortens names, so files are smaller. Code splitting: each page loads only what it needs. Source map: maps minified code back to the original for debugging. Polyfill: adds a modern feature to an old browser. JIT: the engine compiles JS to machine code while it runs. AOT: compile before it runs (C++), predictable speed.
| Front-end JS | Node.js | C++ | |
|---|---|---|---|
| To machine code | At run time (JIT) | At run time (JIT) | Before run time (AOT) |
| Build goal | Small download, browser support | Correct JS for the Node version | Fast, predictable code |
| Type checks | Build time only | Build time only | Compile time |
SEO
SEO: make pages easy for search engines to find, read and rank.
- Send real HTML from the server (SSR/SSG), so search bots can read the content without running JS.
- Good
<title>, meta description (the text under the link in Google), headings, semantic HTML (tags that describe meaning:<nav>,<article>). - Clean URLs,
sitemap.xml(list of all pages),robots.txt(what bots may read), canonical links ("this is the main copy of this page"). - Fast pages;
alttext on images. Next.js: set these withmetadata/generateMetadata.
Core Web Vitals (Google's page-quality scores): LCP (Largest Contentful Paint): how fast the main content shows (< 2.5 s) · INP (Interaction to Next Paint): how fast the page reacts to a click (< 200 ms) · CLS (Cumulative Layout Shift): how much the page jumps while loading (< 0.1).
Real-time: WebSockets, SSE, polling
WebSocket: an open, two-way connection. Starts as HTTP, then upgrades. Example: live fraud alerts on an ops dashboard.
| Method | How | Use for |
|---|---|---|
| Polling | Ask every N seconds | Slow-changing data |
| Long polling | Browser asks; the server keeps the request open until there is new data, then the browser asks again | Old fallback |
| SSE (Server-Sent Events) | One-way stream, server → browser, over normal HTTP | Live payment status, notifications |
| WebSocket | Two-way, always open | Chat, live trading, collaboration |
- Always
wss://(WebSocket over TLS = encrypted). Check the user's login when they connect, and check permissions on each message. - If the link drops, reconnect with backoff (wait 1 s, 2 s, 4 s…). Send a heartbeat (small ping/pong every few seconds) to detect a dead link. Show "data is stale" to the user.
- Many servers → a shared message bus (e.g. Redis pub/sub), so a message on server A also reaches users connected to server B.
CSS (basics)
- Box model: every element is a box: content → padding (space inside the border) → border → margin (space outside).
box-sizing: border-boxmakeswidthinclude padding and border (easier sizing). - Flexbox: one direction (a row of buttons). Grid: two directions (page layout).
- Specificity: when two CSS rules clash, the more specific wins: inline style >
#id>.class> element (div). Avoid!important(it overrides everything and is hard to undo). - Responsive: the layout adapts to screen size. Media queries (
@media (max-width: 600px)), relative units (rem,%), and design for mobile first. - Your link: "Responsive UI improvements at NG; Material UI and component libraries with Storybook at BAE."
Web performance
- Measure first: Lighthouse (Chrome page-speed report), DevTools Performance tab, React Profiler (shows slow re-renders).
- Smaller bundles (less JS to download): code splitting, lazy loading (load a component only when it is needed:
React.lazy,next/dynamic), remove big libraries. - Fewer re-renders: keep state in the lowest component that needs it,
React.memo, stable keys. - Virtualise long lists: render only visible rows (
react-window). Example: 10,000 transactions. - Cache: HTTP caching (browser keeps files), CDN, React Query (keeps API results).
- Images: right size, modern smaller formats (WebP/AVIF),
next/image.
CDN: servers in many places that keep copies of your files; users get them from the nearest one.
Front-end security
- XSS (attacker script runs in your page): React escapes text; never put user data into
dangerouslySetInnerHTML; add a CSP (header that limits which scripts may run). - CSRF (another site sends requests with your cookies):
SameSitecookies, CSRF tokens. - Keep login tokens in httpOnly cookies (JavaScript cannot read them), not
localStorage(any script, including an attacker's, can read it). - No secrets in front-end code. In Next.js only
NEXT_PUBLIC_variables reach the browser. - Card data: use the payment provider's hosted fields (their input boxes inside your page) so card numbers never touch your servers (keeps you out of most PCI rules).
Accessibility
- Semantic HTML (
<button>,<nav>,<label>), so screen readers understand the page. Everything works with the keyboard; the focused item is clearly visible. - Colour contrast; never colour alone for meaning (example: "Declined" in red and with text/icon).
- ARIA (extra attributes for screen readers, e.g.
aria-label) only when normal HTML cannot do it. - Automated checks (axe tools: find missing labels, low contrast) + manual keyboard and screen-reader checks.
- Your link: "I improved automated accessibility coverage across applications at NG."
Front-end testing
- Jest: runs tests and checks results (
expect). React Testing Library: tests what the user sees and does (find by text/role, click), not internal code. - Cypress: E2E (end-to-end: the whole app, like a real user) in a real browser. Storybook: build and show each component on its own, with its different states.
- MSW (Mock Service Worker): fakes API responses, so tests do not call real services.
render(<PaymentForm />);
await userEvent.type(screen.getByLabelText("Amount"), "50");
await userEvent.click(screen.getByRole("button", { name: "Pay" }));
expect(await screen.findByText("Payment approved")).toBeInTheDocument();
Your link: "Jest, RTL and Cypress at BAE; components in Storybook."
30-second answers
"What is SSR and why Next.js?"
"SSR means the server builds the HTML for each request, so the user sees content fast and search engines can read it. Then React hydrates the page to make it interactive. Next.js lets me choose SSR, static or client rendering per page, and gives routing, code splitting, image optimisation and server code in one framework. The trade-off is more complexity and a server to run."
"Front-end vs back-end compilation?"
"On the front end, TypeScript and JSX are transpiled, bundled, tree-shaken, minified and split, because code goes over the network to many browsers. On Node, I transpile and run; bundle size matters much less. Both are JIT-compiled at run time. C++ compiles ahead of time to machine code for one CPU, which gives predictable performance."
"When would you use WebSockets?"
"When the server must push updates in real time, or both sides send often, like a live dashboard. For one-way updates SSE is simpler. I always use TLS, authenticate the connection, and handle reconnects and heartbeats."
A11. TypeScript
| Question | Short answer |
|---|---|
| Why TypeScript? | Errors at build time, safe refactors, self-documenting. Types are gone at run time → validate input (Zod) |
type vs interface | Both describe the shape of an object. interface can be extended and merged; type can also do unions (A | B). Pick one style and be consistent |
any vs unknown | any turns checks off; unknown forces a check. Prefer unknown |
| Generics | A placeholder type, filled in when used, so one function works for many types: function first<T>(items: T[]): T → first<Card>(cards) returns a Card |
| Union + narrowing | A union = "one of these types" (string | number). Narrowing = a check that tells TS which one it is now (if (typeof x === "string")) |
| Utility types | Built-in helpers that make new types from old ones: Partial<T> (all fields optional), Pick<T, K> (only some fields), Omit<T, K> (all but some), Record<K, V> (object with keys K and values V) |
| Structural typing | TS checks the shape (the fields), not the class name. Any object with the right fields fits |
enum vs string union | enum Status { Paid, Failed } creates real JS code. A union "paid" | "failed" creates none. Many teams prefer unions |
as const / readonly | as const: treat values as exact, read-only ("paid", not any string). readonly: a field that cannot change after creation |
?. and ?? | Stop at null/undefined; default only for null/undefined (not 0 or "", unlike ||) |
keyof / typeof | keyof Card = the list of Card's field names. typeof x = "the type of this variable" |
ReturnType, Parameters, Awaited | Get a function's return type / its parameter types / the value inside a Promise, without writing them again |
as (assertion) | "Trust me", no run-time check. Use rarely |
satisfies | Checks a value matches a type, but keeps TS's more exact guess of the type ("inferred" = worked out by TS) |
strict mode | Turns on the strong checks, e.g. strictNullChecks (you must handle null/undefined). Always on |
tsconfig.json | The TS settings file: which JS version to output (target), module type, strictness, folder paths |
.d.ts files | Type-only files that describe a plain JS library, so TS can check its use (@types/express) |
| Mapped / conditional types | Build a new type by looping over another type's fields: { [K in keyof T]?: T[K] } = "every field of T, made optional" (this is how Partial<T> works) |
catch (err) type | err is unknown (anything can be thrown) → check it, e.g. if (err instanceof Error), before use |
Discriminated union: a union with a shared "tag" field; TS narrows the type by the tag.
type Payment = | { kind: "card"; last4: string } | { kind: "paypal"; email: string }; if (p.kind === "card") p.last4; // TS knows p is the card type hereYour link: "One template per notification type; a union like this makes sure each type is handled."
Type guard: a check that tells TS the type, e.g.
typeof x === "string"or a function returningx is Card.
Typed Express handler (your back end):
app.post("/payments", (req: Request<{}, {}, PaymentBody>, res: Response) => {
const body = PaymentSchema.parse(req.body); // run-time check: types vanish at run time
res.status(201).json(body);
});
"Why TypeScript?" (30 seconds)
"It finds many errors at build time, not in production. Refactoring is safer because the compiler shows every place a change breaks. The types also document the code. But types do not exist at run time, so I still validate outside data, like API input, with a schema library."
A12. Testing, CI/CD, configuration and change management
Testing pyramid: many unit (fast) → some integration → few end-to-end (slow)
| Term | Meaning | Your link |
|---|---|---|
| Unit test | Tests one small piece (a function or component) alone, with its dependencies faked | Jest, React Testing Library |
| Integration test | Tests several real parts together (API + database) | — |
| E2E (end-to-end) test | Tests the whole app in a browser, like a real user clicking | Cypress |
| Mock | A fake version of something your code depends on, so the test is fast and controlled | Fake payment gateway that always says "approved" |
| TDD (Test-Driven Development) | Write a failing test first (red) → write code until it passes (green) → clean up the code (refactor) | — |
| Flaky test | Passes and fails with no code change | Your CI/CD story (pipeline time, 10–20%) |
| CI / CD | CI (Continuous Integration): every push is built and tested automatically. CD (Continuous Delivery/Deployment): code that passes is released automatically or with one click | Your pipelines at NG |
| Configuration management | Record exactly which versions of code, tools and documents make up each release. Baseline = an approved, frozen snapshot; changes after it need approval | Git tags, releases |
| Change control | A formal route for every change: change request → impact analysis (what could it break?) → approval → implement → verify | Controlled releases |
| Change Control Board (CCB) | The group that approves changes | — |
| Documentation | In safety work, documents are the evidence; write them with the work, keep them in version control | — |
A13. Git
- Merge joins two branches and keeps the full history (adds a merge commit). Rebase replays your commits on top of the other branch, so history is one straight line. Do not rebase branches other people use: it rewrites history they already have.
- Conflict (two people changed the same lines): pull the latest → choose the right code in the files (talk to the other dev if unsure) → run tests → commit.
- Protected branches + pull/merge requests: nobody pushes straight to
main; every change is reviewed and must pass CI before merge. - Tags: a fixed name for a release commit (
v2.1.0). Semantic versioning: MAJOR.MINOR.PATCH = breaking change . new feature . bug fix. git bisect: Git checks out commits halfway between a good and a bad version; you say "good" or "bad" each time, until it finds the commit that added the bug.revertmakes a new commit that undoes an old one (safe on shared branches).resetmoves the branch back and removes commits from history (use only on your own local work).
A14. Architecture, deployment and good practice
Monolith vs microservices
| Monolith | Microservices | |
|---|---|---|
| Shape | One app | Many small services, one job each |
| Start / test / deploy | Easy | Harder: calls go over the network (can fail), more things to monitor, versions must stay compatible |
| Scale | Copy the whole app | Copy only the busy service |
| Teams | Share one codebase and one release | Each team can release its service alone |
Good answer: "Start with a monolith; split only when there is a clear need, like different scaling or team ownership." Your link: "I designed several microservices at NG, including the notifications service."
Related terms:
API gateway: one front door for all services. It sends each request to the right service, and checks login and rate limits in one place. Service discovery: services find each other by name (
payments-service), not by a fixed IP address that can change. Kubernetes does this with its own DNS. Event-driven: a service announces "PaymentSucceeded"; any other service that cares reacts. The sender does not need to know who listens (loose coupling). Saga: a multi-service transaction made of steps with undo steps. Example: reserve stock → charge card → if charge fails, release stock. Eventual consistency: data in different services agrees after a short delay, not instantly. Example: a payment shows in your banking app a few seconds after the shop says "paid".
Docker and Kubernetes
Docker container: the app plus everything it needs, packed to run the same everywhere. Stops "it works on my machine". Image vs container: the template vs a running copy (like class vs object). Kubernetes: runs and manages many containers: restarts failed ones, scales, rolls out new versions.
| K8s term | Meaning |
|---|---|
| Pod | The smallest unit Kubernetes runs: one (or a few) containers together, with one IP address |
| Deployment | Keeps N copies of a pod running; rolling update = replace old pods with new ones a few at a time, so there is no downtime |
| Service | A stable name and address for a group of pods (pods come and go; the Service name stays) |
| ConfigMap / Secret | Settings given to pods from outside the code / the same for sensitive values (passwords, keys) |
| Liveness / readiness probe | Health checks Kubernetes calls: "Is it alive?" (if not, restart it) / "Is it ready for traffic?" (if not, send no requests yet) |
| Horizontal Pod Autoscaler | Adds pods when load (e.g. CPU use) goes up, removes them when it goes down |
Your link: "Kubernetes and Linux are on my CV; I deploy and debug services there."
Serverless
Serverless (AWS Lambda): you write functions; the cloud runs and scales them; you pay per use. Good: no servers to look after, scales by itself. Bad: cold starts (first call after a quiet time is slow, because a new copy must start), time limits per call, harder to test on your laptop. Your link: "First-of-type serverless solution at BAE: Python, Lambda, S3, DynamoDB, CDK, CloudFormation." Infrastructure as code: define cloud resources (databases, queues, functions) in code files (CDK, CloudFormation, Terraform), reviewed in Git, instead of clicking in a web console. Repeatable and reviewable.
Logging, monitoring, observability
- Logs (text records of what happened), metrics (numbers over time, e.g. requests per second, error rate), traces (the path of one request through all services, with time spent in each).
- Correlation ID: one ID passed through all services for a request, so you can follow it.
- Alert on symptoms users feel (error rate, latency = how long a request takes), not on every small thing, or people start to ignore alerts.
- Never log card numbers, passwords or tokens.
Clean code and good practice
| Practice | One line |
|---|---|
| KISS | Keep it simple, stupid: the simplest solution that works is usually best |
| DRY vs DAMP | DRY (Don't Repeat Yourself): put shared logic in one place. DAMP (Descriptive And Meaningful Phrases): readable code first; tests may repeat a little so each test is clear on its own |
| YAGNI | You Aren't Gonna Need It: do not build features "just in case" |
| Defensive programming | Check inputs and assumptions; never trust outside data |
| Code review | A second person checks every change; review for correctness, tests, readability |
| Static analysis / linting | Tools that check code for bugs and style without running it: ESLint (JS/TS), SonarQube, clang-tidy (C++) |
| Dependency scanning / SBOM | Check your libraries for known security holes (npm audit, Dependabot). SBOM (Software Bill of Materials) = a list of every component and version in your software |
| Technical debt | Quick shortcuts that make future changes slower or riskier (like interest on a loan); track them as tickets. Your link: legacy modernisation |
| Post-incident review | After an outage: find the root cause (the real first reason), no blame, add a test or check so it cannot happen again |
| Feature flags | A setting that turns a feature on/off without a new release; switch it on for a few users first, and switch it off fast if it breaks |
AI-assisted development (safely)
- Approved tools only; no sensitive code or data in public tools.
- Review generated code like a colleague's; tests and reviews do not change.
- Use it for boilerplate (repetitive set-up code), tests, docs; not to skip understanding. The engineer stays responsible.
- Your link: "AI-augmented development at NG while keeping code quality and testing standards."
A15. Live coding round
1. Repeat the problem in your own words
2. Ask about edge cases (empty input? negative amounts? duplicates?)
3. Explain a simple solution first
4. Write code while talking
5. Test with an example
6. Talk about Big O and how to improve
Common topics: arrays and strings (reverse, find duplicates, two-sum = find two numbers that add up to a target), hash maps (count items, fast lookup), recursion (a function that calls itself on a smaller part), sorting, Big O.
Example: "Find duplicate transactions": use a Set of seen IDs → O(n), not a loop in a loop (O(n²)).
A16. Compilation: AOT vs JIT (and interpreted)
First, the basic words
CPU: the chip that runs the program. Machine code: the only language a CPU understands: numbers that mean "add", "copy", "jump". Each CPU family has its own (Intel/AMD x86, phone and board chips ARM). Compile: translate your code (C++, Java, JS) into machine code. Runtime: a program that must run next to your code to make it work. Example: Node.js is the runtime for your JavaScript.
So every language must become machine code at some point. The only question is when.
The two answers: before, or during
AOT (Ahead-Of-Time): compile before the program runs. You ship the finished machine code. Example: C and C++. You build an
.exe(or firmware file) once, then it runs directly on the CPU.
JIT (Just-In-Time): compile while the program runs. Example: your Node.js code. V8 (the engine inside Node and Chrome) starts running your JS at once, then compiles the busy parts into machine code as it goes.
Interpreted: a program reads your code and does each step, with almost no compiling. Simple, but slower. Example: classic Python.
AOT: your code → [compile] → machine code → run (compile BEFORE)
JIT: your code → run → [compile the busy parts] → keep running faster (compile DURING)
Words you will hear in follow-up questions
Hot code: the parts of the program that run very often. Example: the function that handles every API request. A JIT compiles these first, because they give the biggest speed gain.
Warm-up: the slow start of a JIT program, before the hot code is compiled. Example: the first few requests to a new Node server are a bit slower.
Cold start: warm-up in serverless (AWS Lambda). A new function copy starts from nothing, so the first request is slow. Your link: AWS Lambda at BAE.
Bytecode: a middle step between your code and machine code. It is the same on every CPU, so one file runs everywhere. Example: Java
.classfiles.
Virtual machine (VM) in this context: the runtime that runs bytecode and does the JIT. Example: the JVM for Java. (Not the same as a cloud VM.)
Garbage collection (GC): the runtime finds memory you no longer use and frees it for you. It can pause your program for a short time, at moments you do not control.
Inlining: copy a small function's code into the place that calls it, so the program skips the "jump to the function" step. A JIT does this for hot code.
Predictable timing: the same work always takes about the same time. Important in real-time systems, where a late answer is a wrong answer.
Footprint: how much memory and storage the program needs.
Certify: prove to a regulator that software is safe enough (aircraft, air traffic, medical). Easier when the exact machine code is fixed before it runs.
Cross-compile: build on your PC for a different CPU. Example: compile on a laptop (x86) for an ARM board on a vehicle.
PGO (Profile-Guided Optimisation): an AOT trick to get some JIT benefits.
1. Build the program normally 2. Run it with typical work and record which parts are hot (the "profile") 3. Build again, telling the compiler which parts to optimise mostExample: a C++ server built with PGO can be a bit faster, because the compiler knows the hot paths.
Advantages and disadvantages
| AOT (C, C++, Rust, Go) | JIT (Java, C#, JavaScript/Node) | |
|---|---|---|
| Start-up | Fast: machine code is ready | Slow at first (warm-up) |
| Speed after start-up | Fast | Fast, sometimes faster (it optimises hot code using real data) |
| Timing | Predictable | Less predictable (compile work + GC pauses) |
| Memory | Small | Bigger (runtime + compiler in memory) |
| Runs on other CPUs? | Build again for each CPU | Yes, the same code runs anywhere the runtime exists |
| Build time | Longer | Short: edit and run |
Where each one is used
| Job | Choose | Simple reason |
|---|---|---|
| Microcontroller, vehicle control, flight systems | AOT | Must be on time, every time; little memory; must be certified |
| Web APIs and back ends (your Node services) | JIT | Servers run for days, so warm-up does not matter; fast to develop |
| JavaScript in the browser | JIT | Code arrives as text and must run on any device |
| Small scripts, data analysis | Interpreted (Python) | Fast to write; speed is less important |
Some systems mix both (Android; Java and .NET can also compile AOT). It is a trade-off, not "one is better".
30-second answer: "Why choose JIT or AOT?"
"AOT, like C and C++, compiles to machine code before the program runs. It starts fast, uses little memory and has predictable timing, so it suits embedded and real-time systems. The cost is a separate build for each CPU type. JIT, like Java or Node's V8, compiles while the program runs. It runs the same code on any machine and can optimise the busy parts using real data, so it suits long-running servers. The costs are a slow start, more memory and less predictable timing. For a vehicle controller I would choose AOT; for a web API, JIT is fine."
If they dig deeper
| They ask | You say |
|---|---|
| "Why is JIT timing unpredictable?" | "The runtime compiles code and frees memory (garbage collection) at moments you do not control, so some requests take longer." |
| "How can JIT be faster than AOT?" | "It sees how the program really runs, so it can optimise the hot code for real data, for example by inlining a function called millions of times." |
| "Can AOT get that benefit?" | "Partly, with PGO: run the program once, record the hot parts, then compile again with that information." |
| "What is a cold start?" | "In serverless, a new copy of the function starts from nothing, so the first request is slow. AOT builds start faster." |
| "Is JavaScript compiled or interpreted?" | "Both. V8 starts by interpreting it, then JIT-compiles the hot parts to machine code." |
Your link: "My Node services run on V8, which uses JIT. That suits long-running APIs. For real-time or embedded work, I would choose AOT C or C++ for the predictable timing."
PART B: HARDWARE AND LOW LEVEL
Examples here use devices: a card terminal (payments hardware), an aircraft / vehicle sensor, a valve actuator (your Flowserve work).
B1. Memory
Stack: memory for local variables inside a function. Fast, automatic, small. Freed by itself when the function returns. Heap: a big pool of memory you ask for while the program runs (
malloc/new). In C/C++ you must give it back yourself (free/delete). Garbage collection (GC): the runtime frees unused heap memory for you (JS, Java, Python). Easy, but GC pauses are not predictable → avoided in hard real-time code.
| Bug | Meaning |
|---|---|
| Memory leak | Memory never freed → program grows until it crashes |
| Dangling pointer | A pointer to memory that was already freed; using it reads garbage or crashes |
| Buffer overflow | Writes past the end of an array (e.g. 20 chars into a 10-char buffer) → overwrites other data; attackers use this to run their own code |
| NULL pointer use | Using a pointer that points to nothing (address 0) → crash |
| Fragmentation | Free memory is split into many small gaps, so a big request fails even if the total free memory is enough. This is why embedded code avoids the heap after start-up |
B2. C and C++ core
Pointer: a variable that holds a memory address.
int x = 5; int *p = &x; *p = 10; // x is now 10Reference (C++): another name for a variable. Cannot be null, cannot be re-pointed.
Pass by value vs pointer vs reference (C++):
void byValue(int amount) { amount += 1; } // gets a COPY → original unchanged
void byPointer(int* amount) { *amount += 1; } // gets the ADDRESS → original changed
void byReference(int& amount) { amount += 1; } // gets the ORIGINAL → original changed
void readOnly(const Order& o) { /* read only */ } // no copy, and cannot change it
int price = 50;
byValue(price); // price = 50
byPointer(&price); // price = 51
byReference(price); // price = 52
- By value: safe, but copies the data (slow for big objects).
- By reference / pointer: no copy, can change the original. A pointer can be
nullptr; a reference cannot. const T&(const reference): the common choice for big objects you only read: fast and safe.- C has no references: it passes by value, and uses pointers to change the original.
| Keyword | Meaning |
|---|---|
const | The value cannot be changed through this name |
static (local variable) | Keeps its value between calls (not reset each time) |
static (global variable / function) | Visible only inside this .c/.cpp file |
volatile | "This can change outside this code, so always read it from memory" (stops the compiler from caching it). For hardware registers and ISR flags. Not thread-safe |
inline | A hint to copy the function's code into the place it is called, to save the cost of a call |
extern | "This variable exists, but it is created in another file" |
RAII (Resource Acquisition Is Initialisation): an object gets a resource (memory, a file, a lock) when it is created and frees it when it is destroyed, so you cannot forget. Example:
std::lock_guardunlocks a mutex at the end of the{ }block, even if an exception is thrown. Smart pointers: pointers that free memory by themselves.std::unique_ptr(one owner; freed when the owner goes),std::shared_ptr(many owners; it counts them and frees when the count is 0),std::weak_ptr(can look at a shared object but does not keep it alive). Avoid rawnew/delete. Move semantics: hand over a big object's insides (e.g. its memory buffer) to a new owner instead of copying them (std::move). Much faster for big objects. Templates: write code once for any type; the compiler makes a version per type (std::vector<int>,std::vector<Card>). Compile-time polymorphism. Rule of 3/5/0: if your class needs a custom destructor, copy or move function, it probably needs all of them (3 or 5). Best: need none, because your members already clean up after themselves (rule of 0).
C vs C++: C is procedural (just functions and data, no classes), small, common on bare metal. C++ adds classes, RAII, templates, and the STL (Standard Template Library: ready-made containers like vector, map, and algorithms like sort).
Compile pipeline:
source (.c/.cpp) → preprocessor (#include, #define) → compiler (.o) → linker → executable / firmware image
- Preprocessor: pastes in
#includefiles and replaces#definenames before compiling. - Compiler: turns each file into machine code (an object file,
.o). - Linker: joins all
.ofiles and libraries into one program, and connects calls between them.
Endianness: the order of bytes when a number is stored.
0x12345678: big-endian stores12 34 56 78(biggest byte first), little-endian stores78 56 34 12(smallest first). Matters when data goes over a network or between different processors: both sides must agree.
Bit manipulation:
reg |= (1u << 3); // set bit 3
reg &= ~(1u << 3); // clear bit 3
reg ^= (1u << 3); // toggle bit 3
if (reg & (1u << 3)) // test bit 3
B3. Embedded basics
Embedded system: a computer inside a bigger device, made for one job. Example: a card terminal, a flight sensor, a valve controller. Bare metal: no operating system; your code runs straight on the chip, usually
main()with an endless loop. RTOS (Real-Time Operating System): a small OS that guarantees tasks run on time (FreeRTOS, Zephyr, VxWorks). Embedded Linux: full Linux on a board; Yocto and Buildroot are tools that build a custom small Linux image.
| Bare metal | RTOS | Embedded Linux | |
|---|---|---|---|
| Size | Smallest | Small | Big |
| Timing | Fully predictable | Real-time (deadlines guaranteed) | Not hard real-time by default |
| Features | Few | Tasks (like threads), queues, mutexes | Full OS, drivers, networking |
Interrupt / ISR (Interrupt Service Routine): hardware pauses the normal code and runs a short function at once. Rules: keep it short, no waiting (blocking), no
malloc(heap), noprintf(slow). Just set a flag or push data to a queue; do the real work in the main loop. Example: a card is inserted → interrupt → ISR setscardInserted = true→ main loop reads the card. Register: a tiny memory slot inside a hardware part that controls it or shows its state (e.g. "bit 0 = LED on"). Memory-mapped I/O: hardware registers have normal memory addresses, so you read/write them like variables, usingvolatilepointers. Watchdog: a hardware timer that resets the system if the software stops "kicking" (resetting) it in time, e.g. because it froze. DMA (Direct Memory Access): hardware copies data into memory without using the CPU. Example: sensor samples go straight into a buffer while the CPU does other work. Hard vs soft real-time: hard = a missed deadline is a failure (flight control); soft = a missed deadline is only worse (a video frame drops). Polling vs interrupts: polling = the code keeps checking "is it ready yet?"; interrupts = the hardware tells you when it is ready. Interrupts save CPU and react faster.
| Interface | One line |
|---|---|
| GPIO (General-Purpose Input/Output) | One pin you set high/low (e.g. an LED) or read (e.g. a button) |
| UART | Simple serial link, one bit after another, 2 wires (TX send, RX receive). Used for debug consoles. RS-232/RS-485 are UART-style links over longer cables |
| SPI | Fast, 4 wires; one controller talks to many devices, choosing one with a select wire (flash memory, displays) |
| I2C | 2 wires shared by many devices; each device has an address. Slower (sensors) |
| CAN | A robust shared bus (many devices on one cable) for vehicles and industry; handles electrical noise well |
| Ethernet | Networking |
FPGA: a chip you program to become custom digital hardware. Very fast parallel processing. Common in defence and signal processing. Microcontroller vs microprocessor: CPU + memory + peripherals (timers, UART, GPIO) all on one chip, for small devices vs a CPU only, with memory outside it, powerful enough to run Linux. Firmware: software stored on the device that controls the hardware. Bootloader: small program that starts first and loads the main firmware; also used for firmware updates.
Your link: "At Flowserve I worked with pneumatic and hydraulic actuators and tested valves against operating limits. In embedded terms, the controller reads sensors and drives an actuator inside safe limits, with a watchdog and fail-safe state."
B4. Threads at low level
Mutex: a lock; only one thread can hold it at a time. Others wait. Semaphore: a counter for a limited resource (e.g. 3 free buffers), or a signal from one task to another ("data ready"). Atomic: an operation that completes in one step, so no other thread sees it half-done (
std::atomic<int>, C11stdatomic.h). Condition variable: lets a thread sleep (use no CPU) until another thread signals "something changed". Priority inversion: a low-priority task holds a lock that a high-priority task needs; a medium task keeps running and blocks both. Fix: priority inheritance (the low task briefly gets high priority to finish fast). Famous case: Mars Pathfinder, 1997. Producer–consumer: producer → thread-safe queue (mutex + condition variable) → consumer. Example: sensor ISR produces samples, a task consumes them.
Debugging threads: reproduce under heavy load → logs with timestamps and thread IDs → ask "what data is shared, and what protects it?" → tools: ThreadSanitizer / Helgrind (find races), GDB (info threads, thread apply all bt = show where every thread is) → fix → stress test (run it hard, many times).
B5. Debugging tools
| Tool | Use |
|---|---|
| GDB | Debugger: stop at a line (breakpoint), step line by line, look at variables and memory |
| JTAG / SWD | A hardware connection to the chip's pins, so a debugger can stop and inspect code running on the real board |
| Valgrind / AddressSanitizer | Find memory leaks and bad memory use (overflows, use after free) |
| ThreadSanitizer | Race conditions |
| Logic analyser / oscilloscope | Show the real electrical signals on wires: on/off patterns (logic analyser) or voltage over time (oscilloscope) |
strace | Shows every request a Linux program makes to the OS (open file, read, connect), so you see where it fails or waits |
B6. Linux
| Command | What it does |
|---|---|
ps / top | List running processes / live view of CPU and memory use |
grep | Search text in files or output |
tail -f | Follow a log file live as new lines are added |
journalctl / systemctl | Read system service logs / start, stop, check a service |
chmod | Change file permissions |
ssh / scp | Log in to another machine / copy files to it |
df | Show free disk space |
ss | Show network connections and open ports |
dmesg | Kernel messages (hardware and driver problems) |
make / gcc | Build a project / compile C code |
Signal: OS message to a process.
SIGINT(Ctrl+C),SIGTERM(please stop),SIGKILL(stop now),SIGSEGV(bad memory access).
Live issue on Linux: is the service running (systemctl status) → what do the logs say (journalctl, tail -f) → out of CPU, memory or disk (top, df) → network OK (ss) → reproduce in test → fix → release under change control.
Your link: "I use Linux daily: Bash scripts, containers and Kubernetes."
B7. Safety-critical and embedded practice
| Term | One line |
|---|---|
| Safety-critical | A failure can hurt people (flight control, braking) |
| MISRA C / C++ | A set of safe-coding rules for C/C++ in cars, aircraft, defence (e.g. no heap after start-up, no exceptions) |
| Static analysis | Check code without running it (cppcheck, clang-tidy, MISRA checkers) |
| HAL (Hardware Abstraction Layer) | Thin layer of functions that hide the hardware (led_on() instead of writing a register), so you can swap in a fake in tests |
| Unit tests on host | Test the logic on your PC (the "host"), not the board, with a fake HAL (Unity/Ceedling for C, GoogleTest for C++) |
| HIL (Hardware-in-the-Loop) testing | Run the real software on real or simulated hardware, as part of the CI pipeline |
| Cross-compile | Build on your PC for another CPU (ARM) |
| V-model | A safety-friendly process: each design stage on the left (requirements → system design → detailed design → code) has a matching test stage on the right (acceptance ← system ← integration ← unit test) |
| Requirements traceability | Each requirement is linked to its design, code and tests, so you can prove every requirement was built and tested |
| Redundancy | A backup that takes over if the main part fails; it must not fail in the same way (e.g. same bug) |
| Fail-safe vs fail-operational | On a fault, go to a safe state (stop, alarm) vs keep working (e.g. reject only the bad input) |
| Assurance levels | How much evidence software needs, based on how bad a failure could be (DO-178C for aircraft, ED-153 for ATM ground systems) |
Embedded CI/CD:
Push → cross-compile → static analysis → unit tests on host → flash to test board → HIL tests → signed release
("Flash" = write the firmware onto the board's memory. "Signed release" = the release file has a digital signature, so the device can check it is genuine.)
Your link: "My CI/CD work transfers directly: same pipeline idea, plus cross-compiling and hardware-in-the-loop stages."
PART C: LAST-MINUTE QUICK-FIRE LIST
Say each answer in one breath:
- Class vs object: template vs one instance (
Cardclass vs your card). 1b. Method vs function: belongs to a class/object (card.charge()) vs stands alone (addVat()). 1c. Pass by value vs reference: a copy (original unchanged) vs the original (changes affect it). JS: numbers/strings by value, objects by reference. - Encapsulation: hide data, change it only through methods (card number private, only last 4 shown; balance only via
charge()). - Abstraction: show what, hide how (call
pay(); the steps to pay are hidden inside). - Inheritance: child inherits the parent's properties and methods, can add extra ones and override them, "is-a" (CreditCard is a Card; shared checks written once).
- Polymorphism: same call, different behaviour (one
pay()→ card or PayPal). 5b. public / private / protected: anyone / only this class / this class + subclasses (pay()/balance/checkExpiry()). Start private. - Overloading vs overriding: same name, different parameters, same class, compile time vs child replaces parent method, run time.
- Operator overloading: custom meaning for
+,==(Money + Money). C++ yes; Java/JS/TS no. - Interface vs abstract class: list of methods with no code, a class can implement many vs a base class with some shared code, a class can extend only one.
- Composition vs inheritance: has-a vs is-a; prefer composition.
- SOLID: one job per class, add new classes instead of editing old ones, a child works wherever the parent works, small interfaces, depend on interfaces not specific classes.
- Idempotent: retry = same result (idempotency key → no double charge).
- ACID: database transactions are all-or-nothing, follow the rules, do not mix, and survive a crash (e.g. a transfer).
- Race condition: two threads, shared data, timing decides (double spend).
- Deadlock: wait for each other; same lock order.
- Concurrency vs parallelism: taking turns vs at the same time.
- JS threads: one thread + event loop; workers for CPU work; async ≠ parallel.
- Exceptions: unexpected and unrecoverable → throw; expected (card declined) → return a result.
- TLS: encrypts traffic, detects changes, proves the server's identity; uses a certificate (server ID) and a shared session key.
- Stack vs heap: automatic local vs manual (or GC).
volatile: always read memory; for registers/ISR flags; not thread-safe.- RAII: a resource is freed automatically when its object is destroyed (
lock_guardunlocks at end of block). - ISR rules: short, no blocking, set a flag.
- Bare metal / RTOS / Linux: no OS / real-time OS / full OS.
- Fail-safe: safe state on fault.
- JIT vs AOT: compile while running (portable, smart optimisation, warm-up, less predictable) vs before running (fast start, predictable, small; one build per CPU). Real-time/embedded → AOT.
If you go blank: "Good question, let me think for a moment." Then use the formula: definition → example → why → trade-off.