EL/ FIELD GUIDE
QRT + JUMPPYTHON · SQL · KAFKA
Portfolio ↗
Core Python for Java Engineers08/31

4. Types and Clean Architecture

Interview outcome

Use Python's type system to express boundaries without turning the code into Java, and design components that are replaceable in tests and production.

Gradual typing, honestly stated

Type hints are metadata. Static checkers, IDEs, linters, and readers use them; CPython does not generally reject a wrong argument at runtime.

Annotate public boundaries and non-obvious domain structures. Let obvious local variables infer. A type checker is strongest when Any is rare and untyped I/O is validated at the boundary.

from typing import NewType

OrderId = NewType("OrderId", str)
StrategyId = NewType("StrategyId", str)

NewType helps static tools distinguish values represented by the same runtime type. It does not create a runtime wrapper.

Protocols over inheritance

from collections.abc import Iterable
from typing import Protocol

class PriceSource(Protocol):
    def prices(self, symbol: str) -> Iterable[float]: ...

class SignalEngine:
    def __init__(self, source: PriceSource) -> None:
        self._source = source

Any object with the required method satisfies the structural contract to a static checker. This enables small fakes without forcing a shared base class. Use an abstract base class when shared runtime identity, registration, or implementation is genuinely part of the contract.

Model states so invalid transitions stand out

from dataclasses import dataclass
from enum import Enum, auto

class OrderStatus(Enum):
    CREATED = auto()
    SENT = auto()
    ACKNOWLEDGED = auto()
    PARTIALLY_FILLED = auto()
    FILLED = auto()
    CANCELLED = auto()
    REJECTED = auto()

TERMINAL = {OrderStatus.FILLED, OrderStatus.CANCELLED, OrderStatus.REJECTED}

@dataclass(frozen=True, slots=True)
class OrderUpdate:
    order_id: str
    status: OrderStatus
    cumulative_quantity: int

Do not model a state machine as unrelated booleans (is_sent, is_filled, is_cancelled) that can represent impossible combinations.

Dependency direction

Keep the domain independent from databases, brokers, and frameworks:

entry point → application service → domain
                 ↓
             ports/protocols
                 ↑
      broker, database, file adapters

The domain defines what it needs. Adapters implement it. Tests supply in-memory implementations. This is dependency inversion without ceremony.

Validation belongs at boundaries

External dictionaries/JSON are untrusted. Parse once into domain values, reject unknown or invalid states, then let internal code operate on stronger invariants. Do not spread dict[str, object] through the system.

Separate:

  • transport schema (wire compatibility);
  • validated domain event (business meaning);
  • storage schema (query and retention concerns).

One class rarely serves all three well.

Composition versus inheritance

Prefer a strategy that receives a signal function and risk policy over a deep hierarchy of AbstractBaseMeanRevertingEquityStrategy. Inheritance is appropriate when substitutability is stable and tested. Composition keeps independent axes of change independent.

Packaging rules for an interview project

  • src/ layout prevents tests from accidentally importing the working directory.
  • one package with cohesive modules beats many tiny deployment units;
  • use absolute imports across packages and relative imports sparingly within one;
  • public API is explicit; leading underscore marks implementation detail;
  • configuration enters at the composition root, not through imports of globals.

Drill

Define protocols for Clock, EventStore, and ExecutionGateway. Design a TradingService that validates an order, checks risk, persists intent, submits, and records the result. Explain what happens if the gateway times out after the venue accepted the order.

The senior answer does not blindly retry. It uses a stable client order ID, marks the result unknown, queries/reconciles, and makes recovery repeatable.

Answer frame

I use typing to make boundaries and domain states visible, not to simulate Java. Protocols define the behavior a consumer needs. Domain logic depends on those ports, while I/O adapters depend inward. Runtime validation occurs once at untrusted boundaries.