Revision Lookup Sheet — Siemens L2¶
Last day revision reference. Granular detail. No fluff.¶
1. Azure Functions — Complete Reference¶
Hosting Models¶
| Plan | Cold Start | Scale to Zero | VNET | Use When |
|---|---|---|---|---|
| Consumption | Yes | Yes | No | Sporadic, cost-sensitive |
| Premium | No (pre-warmed) | No | Yes | Consistent traffic, VNET needed |
| Dedicated | No | No | Yes | Already have App Service VMs |
| Container Apps | Depends | Configurable | Yes | Custom runtime, containerised |
Cold Start — What Happens¶
- Azure allocates VM from pool
- Container spins up
- Functions host runtime initialises
- Isolated worker process starts (.NET runtime loads)
Program.csruns — DI container built- Only now does your function code run
Infra level — Azure owns. Can't control. App level — DI graph construction + JIT compilation. You control.
Mitigations:
- Reduce DI registrations — smaller graph, faster boot
- Native AOT — eliminates JIT, cold start under 1 second vs 2-4 seconds regular .NET 8
- ReadyToRun (<PublishReadyToRun>true</PublishReadyToRun>) — middle ground, pre-compiles IL
- Premium plan — pre-warmed instances, no cold start on scale out either
Cold start happens at: - First invocation after idle (~20 min on Consumption) - Every scale-out — new instance cold starts
InProc vs Isolated¶
InProc:
- Function code runs in same process as Functions host
- Shared memory, shared DI
- .NET 6 and below, being deprecated
- Can use HttpRequest directly — same process, shared memory
Isolated:
- Separate worker process from host
- Communicates via gRPC
- Full DI container control via Program.cs
- .NET 8 canonical
- Uses HttpRequestData not HttpRequest — objects serialised over gRPC, can't pass live objects across process boundary
Trigger Types¶
HTTP Trigger:
[Function("MyFunction")]
public HttpResponseData Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req)
- Auth levels:
Anonymous,Function(key required),Admin(master key) - Not real auth — key-based obscurity
- Real auth: Easy Authentication (infrastructure JWT validation) or manual JWT in code
- Use for: webhooks, third party callbacks, lightweight APIs
- Cold start risk — not suitable for user-facing high-frequency APIs
Timer Trigger:
[Function("TimerFunction")]
public void Run([TimerTrigger("0 */5 * * * *")] TimerInfo timer)
- 6-part CRON — seconds first:
{sec} {min} {hour} {day} {month} {dow} timer.IsPastDue— whether execution is late/catching uptimer.ScheduleStatus.Last/.Next- Single instance only — no scale out, concurrent timer executions would cause duplicates
Queue Trigger:
[Function("QueueFunction")]
public void Run([QueueTrigger("myqueue", Connection = "AzureWebJobsStorage")] string message)
- Fires on message in Storage Queue or Service Bus
- Scales on queue depth AND message age — both signals together
maxDequeueCount— retry limit before poison queue (default 5)batchSize— messages per instance per batch (default 16)newBatchThreshold— fetch next batch when in-flight count drops below this
Blob Trigger:
[Function("BlobProcessor")]
public async Task Run(
[BlobTrigger("originals/{name}", Connection = "BlobStorageConnection")] Stream stream,
string name)
- Polling based — up to 10 min latency on Consumption
- Receipt system tracks processed blobs in
azure-webjobs-hostscontainer inAzureWebJobsStorage - Delete receipts → reprocesses all blobs
- Stream parameter: can be
Stream,byte[],string,BlobClient {name}bound from path pattern — available as method parameter
Event Grid Trigger:
[Function("EventGridProcessor")]
public async Task Run([EventGridTrigger] CloudEvent cloudEvent)
- Push based — sub-second latency
- Requires Event Grid subscription configured in portal
- Subject filter required — without it, output blobs trigger infinite loop
CloudEventis canonical (CNCF standard),EventGridEventis legacy Azure proprietary- Extract blob URL from
cloudEvent.Data.ToObjectFromJson<StorageBlobCreatedEventData>().Url
Blob Trigger vs Event Grid Trigger:
| Blob Trigger | Event Grid | |
|---|---|---|
| Latency | Up to 10 min | Sub-second |
| Setup | Simple, just code | Portal subscription config required |
| Infinite loop risk | No — path scoped | Yes — needs subject filter |
| Scale signal | Receipt polling | Event rate |
| Production recommendation | Prototyping/low volume | Production |
Scale Controller¶
Azure-managed component. Not your code. Watches trigger sources and makes scaling decisions.
- HTTP — request queue depth
- Queue — depth AND message age together
- Blob — unprocessed blob count via receipts
- Timer — never scales out
- Event Grid — incoming event rate
Max instances on Consumption: 200
Project File Structure¶
Program.cs:
var builder = FunctionsApplication.CreateBuilder(args);
builder.ConfigureFunctionsWebApplication();
builder.Services
.AddApplicationInsightsTelemetryWorkerService()
.ConfigureFunctionsApplicationInsights();
// Singleton clients here — not inside function body
builder.Services.AddSingleton(_ => new BlobServiceClient(
new Uri("https://averblobstore.blob.core.windows.net"),
new ManagedIdentityCredential()
));
builder.Build().Run();
host.json:
{
"version": "2.0",
"functionTimeout": "00:10:00",
"extensions": {
"blobs": { "maxDegreeOfParallelism": 4 }
},
"logging": {
"applicationInsights": {
"samplingSettings": { "isEnabled": true }
}
}
}
local.settings.json (never commit — always in .gitignore):
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "connection string → averimagestore",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"BlobStorageConnection": "connection string → averblobstore"
}
}
In Azure (production) — Managed Identity, no connection strings:
{
"AzureWebJobsStorage__accountName": "averimagestore",
"AzureWebJobsStorage__credential": "managedidentity",
"BlobStorageConnection__accountName": "averblobstore",
"BlobStorageConnection__credential": "managedidentity"
}
.csproj required packages:
<OutputType>Exe</OutputType>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
Microsoft.Azure.Functions.Worker
Microsoft.Azure.Functions.Worker.Sdk ← generates function.json from attributes
Microsoft.Azure.Functions.Worker.Extensions.Blob
Microsoft.ApplicationInsights.WorkerService
Microsoft.Azure.Functions.Worker.ApplicationInsights
Each trigger type needs its own extensions package.
Function Class Rules¶
- Constructor injection works — class resolved from DI per invocation
FunctionContextis optional — only include if you needInvocationIdor manual DI resolutionBlobServiceClientmust be singleton — never new it up inside the function bodystream.Position = 0required between multiple reads — blob streams are seekableManagedIdentityCredentialdirectly in Functions (notDefaultAzureCredential) — Functions always on Azure managed infra, no need for credential chain
Input and Output Bindings¶
Input bindings — pull additional data automatically when function fires:
[BlobInput("metadata/{name}.json")] string metadata // fetch blob
[TableInput("Customers", "{customerId}")] Customer c // fetch table row
[CosmosDBInput(...)] MyDoc doc // fetch CosmosDB document
Output bindings — write result automatically:
[BlobOutput("thumbnails/{name}")]
public async Task<byte[]> Run(...) // return value written to blob
Binding expressions {name} bind to trigger parameters by name.
Local Development¶
Canonical: Azurite + func start + UseDevelopmentStorage=true
- No real Azure resources
- Works offline
- Safe for teams
What you did: real averblobstore connection string locally
- Works but risks real data writes during testing
2. Distributed Systems — Consistency Models¶
CAP Theorem¶
- C — Consistency: every read gets the most recent write
- A — Availability: every request gets a response
- P — Partition Tolerance: system works despite network failures between nodes
P is non-negotiable. Networks fail. Real choice is always CP vs AP.
- CP — reject requests rather than return stale data. PEPPOL is CP — wrong invoice is worse than no invoice.
- AP — always respond, might be stale. Dashboard/reporting is AP — slightly stale data acceptable.
CAP is a per-operation choice, not a per-system choice. Different parts of the same system can make different choices.
PACELC — more practical: even without partition, tradeoff between latency and consistency exists.
Consistency Models (weakest to strongest)¶
Eventual consistency: If no new updates are made, all replicas converge to the same value. Reads may return stale data. System guarantees convergence, not timing.
Example: DNS propagation, product catalog reads, ops dashboard.
Causal consistency: If A caused B, everyone sees A before B. Read-your-own-writes is a subset — you always see your own latest write, others may not yet.
Your implementation: LSN tracking in Redis. After a write, store the Postgres LSN. Route reads to a replica that has caught up to at least that LSN.
Session consistency: Within a session you always see your own writes. Outside the session, eventual.
Strong consistency (linearisability): Every read sees the most recent write. Globally ordered. Feels like a single node.
Requires coordination between nodes before responding — costs latency.
External consistency (Google Spanner): Transactions appear to execute at a real timestamp, globally consistent across datacenters. Achieved via TrueTime API — atomic clocks and GPS bound clock uncertainty to 1-7ms. Spanner waits out the uncertainty window before committing.
The Clock Skew Problem¶
Two write nodes in different regions. Both think a transaction happened at "10:00:00.000". Which was first? Clocks drift — you can't know without bounding the uncertainty.
Why this doesn't affect you: single Postgres primary write node. All writes go through one clock. No skew possible. Spanner's problem is self-inflicted by having write nodes globally for sub-100ms write latency.
Your Consistency Choices in PEPPOL¶
- Invoice processing critical path — strong consistency via Postgres transaction
- Outbox + status written atomically — same transaction
- Ops dashboard reads — eventual consistency acceptable, reads from primary or replica
- Read-your-own-writes for reconciliation — causal consistency via LSN tracking
3. PEPPOL Architecture — Defence Points¶
Distributed Locking — Lock After Read Pattern¶
UPDATE invoices
SET status = 'processing'
WHERE invoice_id = @id AND status = 'uninitiated'
-- Rowcount = 1 → you own it
-- Rowcount = 0 → back off
No Redis, no external lock manager. Database is the lock. Atomic conditional UPDATE under Read Committed — row lock forces WHERE re-evaluation against committed state.
Why not lock before read: crash after lock, before processing → invoice stuck in locked state, manual intervention needed. Lock after read → crash rolls back transaction, status stays 'uninitiated', watchdog picks it up.
Worst case: two processes do full work, only one wins the UPDATE. Known tradeoff — wasted work in rare race condition is acceptable. Alternative (lock first) creates stuck state on crash.
Outbox Pattern¶
Write invoice payload + set status to 'processing' in single Postgres transaction. If crash before commit → rollback, status stays 'uninitiated'. Outbox worker reads committed rows and dispatches. Eliminates dual write problem.
Transaction boundary covers: - Invoice status update - Outbox row insert - ReconKey - PeppolId
All or nothing.
Idempotency — Three Layers¶
- Message deduplication — same message on queue cannot be processed twice (MQ level)
- Invoice deduplication — same invoice ID cannot be processed twice regardless of how it arrived (domain level)
- Network idempotency key — tracks request across vendor network for 24hr ambiguity window (external API level)
Watchdog¶
Reconciles undelivered invoices. Runs on schedule, queries for invoices in 'processing' state past expected delivery window, checks vendor status, updates local state. Self-healing — no manual intervention for network failures.
24hr Ambiguity Window¶
External network failure after dispatch — vendor received invoice but confirmation never arrived. 70-minute detection window is deliberate. 24-hour ceiling is vendor-contractually justified. Residual double-send risk handled via credit note. Accepted tradeoff — simpler than alternative detection mechanisms.
Publisher Confirms¶
Guarantee broker received message before returning success to caller. Without confirms — message could be lost between producer and broker with no indication. Outbox pattern + publisher confirms together guarantee at-least-once delivery.
4. Pricing Engine — The Dragon Story¶
Before (CalculateActualPrice)¶
- 6000 lines, functions within functions
- 25+ sequential DB calls per product
- 1500 columns loaded, 120 used
- 60 chained string replacements to build formula
- Roslyn CSharpScript — full C# compilation engine per calculation
- Everything in the application waited for it
Root Cause¶
Fear crystallised into code. Defensive fetch-everything because no one documented what was actually needed. Engineers added data fetches defensively rather than understanding the domain.
The Pattern — Load Transform Compute¶
Load: Two queries for the entire batch regardless of size. 1 product or 14,000 — database touched exactly twice.
Transform: Single lean context object built entirely in memory. Everything formula evaluation needs — price types, customer deals, channel pricing, excise values, currency rates — all in memory. No DB inside computation.
Compute: NCalc replacing Roslyn. Purpose-built expression evaluator, caches compiled formulas. No compilation overhead, no cold start.
Results¶
- Orders: 4s → 50ms
- Invoices: 3s → 50ms
- Pricing: 5s → 100ms
- Offline file generation 14K products: 10 mins → 8 secs
- EPOS cold start: 40s → 6s (same pattern applied)
Key Classes¶
PriceCalculationEngine— orchestrates Load Transform ComputePriceCalculationContextBuilder— Transform phase, pure in-memoryFormulaEngine— Compute phase, NCalc evaluationSetPriceRouter— pure routing logic, no side effects, testablePriceHierarchyResolver— clean decision tree, single responsibility
The Quote¶
"Dragons in software engineering are mostly lizards standing in dramatic lighting."
The shadow was fear. The shadow was defensive architecture. Once you see it clearly, the shadow is optional.
5. Architecture Decisions — Why Each One¶
RabbitMQ over Azure Service Bus¶
- Cloud-agnostic — same config works on VPS
- Self-hosted — no vendor lock-in
- 7K/day is steady throughput not bursty — Service Bus benefits (scaling, serverless) don't apply
- Translatable later if needed
Docker Swarm over AKS¶
- Cloud-agnostic — same stack file works on any VPS
- No Kubernetes complexity for current scale
- Stateful infra (RabbitMQ, Postgres) pinned or externalised, stateless services float
Service Principal over Managed Identity (Swarm containers)¶
- Containers don't reliably access IMDS — VM identity not automatically inherited by Docker containers
- Stack must also work on VPS where Managed Identity doesn't exist
- Three Docker Secrets:
azure_tenant_id,azure_client_id,azure_client_secret entrypoint.shreads secrets, exports as env vars, starts appDefaultAzureCredentialresolves viaEnvironmentCredential
Managed Identity in Azure Functions (not Service Principal)¶
- Functions run on Azure managed infra — IMDS accessible by default
- System-assigned MI,
Storage Blob Data Contributorrole onaverblobstore - No secrets, no rotation, no Docker Secrets needed
Key Vault over .env files¶
- No credentials on disk
- Single
AddAzureKeyVault()call layers on ASP.NET Core config pipeline - Secret rotation without redeployment
IDocumentStore abstraction over direct Blob SDK¶
- Domain code never references Azure SDK directly
DocumentStoreFactoryswitches on provider config- Same code works on VPS with local file system, Azure with Blob Storage
Consumption plan for image processing, not invoice pipeline¶
- Image processing: sporadic, stateless, latency-tolerant → Consumption correct
- Invoice pipeline: 7K/day steady throughput, compliance webhooks, cold start unacceptable → Swarm always-on correct
6. Key Reference Values¶
| Resource | Value |
|---|---|
| Swarm manager private IP | 10.1.0.4 |
| Swarm manager public IP | 20.219.66.48 |
| Swarm worker private IP | 10.1.0.5 |
| Swarm worker public IP | 52.140.55.120 |
| VNet address space | 10.1.0.0/16 |
| Swarm address pool | 10.20.0.0/16 |
| Azure region (VMs) | South India |
| Blob Storage region | West India |
| Key Vault | peppol-vault |
| Blob Storage account | averblobstore |
| Host storage account | averimagestore |
| GHCR image | ghcr.io/abhishek052off/averazure |
| Resource group | learn_week_1 |
| Function App | image-processor |
| Blob trigger path | originals/{name} |
| Receipt container | azure-webjobs-hosts in averimagestore |
7. Terminology — Names for Things You Use Daily¶
| Term | What it is |
|---|---|
| TPL (Task Parallel Library) | System.Threading.Tasks — Task.WhenAll, Task.WhenAny, Parallel.ForEach, CancellationToken |
| InProc | In-process hosting — function runs in same process as host |
| Isolated | Out-of-process hosting — separate worker process, gRPC communication |
| Outbox pattern | Write message intent atomically with operation, worker dispatches later |
| Idempotency | Operation produces same result regardless of how many times executed |
| Distributed lock | Atomic conditional UPDATE — WHERE status = 'uninitiated' |
| Publisher confirms | Guarantee broker acknowledged message before returning success |
| Dead letter queue | Queue for messages that exhausted retry attempts |
| Eventual consistency | Replicas converge given time, reads may be stale in interim |
| Causal consistency | Read-your-own-writes — you always see your own latest write |
| LSN | Log Sequence Number — Postgres write position, used for replica tracking |
| LTCP | Load Transform Compute Persist — your batch context pattern |
| NCalc | Lightweight expression evaluator — replaced Roslyn in pricing engine |
| IMDS | Instance Metadata Service — Azure endpoint containers use for Managed Identity |
| Blob receipt | Record in azure-webjobs-hosts tracking which blobs Functions has processed |
| Scale Controller | Azure component watching trigger sources and making scale decisions |
| CloudEvent | CNCF open standard for event schema — canonical for Event Grid |
| Subject filter | Event Grid subscription filter — prevents infinite loops on output blob writes |
8. Interview Answers — Cold, Clean, One Line Each¶
What is the outbox pattern? Write the intent to publish a message atomically alongside your domain operation. A worker reads committed rows and dispatches. Eliminates dual write — no state where operation succeeded but message was never sent.
What is idempotency? An operation is idempotent if performing it multiple times produces the same result as performing it once. Implemented via unique tokens checked before processing.
What is CAP theorem? In a distributed system you can only guarantee two of three: Consistency, Availability, Partition Tolerance. P is non-negotiable — real choice is CP vs AP.
What is eventual consistency? If no new updates are made, all replicas will converge to the same value. Reads may return stale data in the interim.
What is a cold start? The lag between trigger and execution when a Functions instance spins up. Caused by VM allocation, container startup, runtime init, and DI graph construction.
What is distributed locking in your system?
Atomic conditional UPDATE — WHERE invoice_id = @id AND status = 'uninitiated'. Row lock forces WHERE re-evaluation. No Redis needed at our scale.
Why RabbitMQ over Service Bus? Cloud-agnostic, self-hosted, same config on VPS. 7K/day is steady throughput — Service Bus serverless scaling adds no value.
Why Swarm over AKS? Same stack file works on any VPS. No Kubernetes complexity justified at current scale. Cloud-agnostic by design.
Why Service Principal over Managed Identity in containers? Docker containers on VMs don't reliably access IMDS. Stack also runs on VPS where Managed Identity doesn't exist. Service Principal works everywhere.
What is LTCP? Load all required data upfront in bulk queries → Transform into in-memory context → Compute purely in memory → Persist. No DB as runtime state. Two queries for the entire batch regardless of size.
What is the dragon story? 6000-line pricing method with 25 sequential DB calls per product, 1500 columns loaded to use 120, Roslyn CSharpScript per calculation. Rebuilt with LTCP — two batch queries, in-memory context, NCalc. Orders 4s→50ms, invoices 3s→50ms, pricing 5s→100ms. It was never a dragon. It was a lizard casting a huge shadow because of how the lighting was set up.