Interview Handbook by Sayed Elmahdy العربية

Topic 26 · Engineering foundations

Legacy systems & integrations

SOAP/WCF, anti-corruption layers, strangler fig, migrating from .NET Framework.

18 questionsJunior · 4Mid · 9Senior · 510 quiz

Concepts

What you will learn: Work with a legacy system or external partner without letting its protocol shape the domain. Follow the path from reading a contract to consuming a service, isolating it and migrating pieces while preserving behavior.

Before you start: It is enough to understand request/response and that a service owns data or an operation. is a message protocol with a contract; describes operations and data types; is a structured text format used to carry them. If these terms are new, begin with the first two cards before comparing migration paths.

Select a term with a dotted underline for its meaning, then continue reading from the same place.

#SOAP, WSDL and XML: start with the contract

A modern store may still need an older invoicing service. is an messaging protocol, not simply any XML sent over . Messages contain an Envelope, an optional Header, and a Body holding an operation or Fault. describes operations, messages, bindings and endpoints; defines data types.

WSDL is an agreed form: field names and types are known before a request is sent. Changing a namespace can break the contract even when element names look identical.
<!-- (1) SOAP 1.1 envelope namespace -->
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Body>
    <!-- (2) Operation namespace belongs to the service contract -->
    <GetInvoice xmlns="urn:store:invoices">
      <invoiceId>42</invoiceId>
    </GetInvoice>
  </s:Body>
</s:Envelope>
🧾 Code walkthrough
  1. (1) This is SOAP 1.1's namespace; SOAP 1.2 uses a different namespace and HTTP content type.
  2. (2) Both the operation and invoiceId are in the service namespace. Prefix spelling is arbitrary; the matters.

Choose a binding matching the WSDL. SOAP is not restricted to HTTP, and WS-Security is distinct from . Preserve an existing contract when consumers depend on it; and are not wire-compatible replacements.

#Consuming SOAP from .NET 10

Generate a proxy from a trusted using dotnet-svcutil or Visual Studio's Web Service Reference instead of assembling manually. The tool adds suitable System.ServiceModel packages, such as System.ServiceModel.Http and System.ServiceModel.Primitives. These provide client support, not the original WCF server.

dotnet tool install --global dotnet-svcutil
# (1) Local, reviewed WSDL and its imported schemas
dotnet-svcutil ./contracts/invoices.wsdl
dotnet restore
using System.ServiceModel;

// (2) Generated names depend on your WSDL.
var binding = new BasicHttpsBinding
{
    SendTimeout = TimeSpan.FromSeconds(15),
    MaxReceivedMessageSize = 1_000_000
};
var endpoint = new EndpointAddress("https://partner.example/Invoices.svc");
var client = new InvoiceServiceClient(binding, endpoint);
try
{
    var result = await client.GetInvoiceAsync(42); // (3)
    client.Close();
}
catch
{
    client.Abort(); // (4)
    throw;
}
🧾 Code walkthrough
  1. (1) Version the contract and schemas, and review generated-code changes when refreshing them.
  2. (2) This assumes a generated InvoiceServiceClient with an int operation argument; adapt both to Reference.cs. BasicHttpsBinding suits 1.1 over , not every binding.
  3. (3) The call is asynchronous, but generated methods may not accept CancellationToken. SendTimeout bounds the operation here; verify authentication, security and quotas against the partner.
  4. (4) If the call or Close fails, Abort cleans up the channel. Do not reuse faulted clients or share them concurrently without a documented guarantee.
WaitAsync with a timeout only stops waiting; it does not prove the service stopped executing. Payments and invoice creation need and reconciliation before resending.

Source: Microsoft's WCF client generation guide.

#Parsing external XML safelyBonus

Optional but valuable: not asked in every interview, but knowing it deepens your understanding and sets you apart.

An payload can attempt external-entity resolution or consume excessive memory. When parsing XML yourself, prohibit DTDs and bound the input size. Use a parser or serializer rather than string concatenation for escaping and namespaces.

using System.Xml;
using System.Xml.Linq;

var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit, // (1)
    XmlResolver = null,
    MaxCharactersInDocument = 1_000_000 // (2)
};
using var reader = XmlReader.Create(inputStream, settings);
var document = XDocument.Load(reader); // (3)
🧾 Code walkthrough
  1. (1) DTDs are rejected, and the resolver cannot open external files or URLs.
  2. (2) Choose a contract-appropriate limit alongside transport and decompression limits.
  3. (3) XDocument loads the document into memory; use streaming XmlReader for large files. Parsing does not replace or business validation.

#An anti-corruption layer at the boundary

A worked example
  1. A legacy system returns abbreviated fields and codes unrelated to your application's model. Spreading them everywhere couples many layers to its changes.
  2. An anti-corruption layer validates and translates at the boundary, for example mapping legacy status codes to your own order states.
  3. Specify unknown values and temporary failures rather than converting every error into a successful default. Protect model meaning, not merely property names.

If a partner calls a status X9, that should not become a domain concept everywhere. An adapter translates interfaces; an anti-corruption layer also translates meaning: currencies, quantity units, time zones and failures.

A translator between teams preserves each team's vocabulary while translating intent, not just words.
public record InvoiceStatus(bool Paid);
public record LegacyInvoice(string StateCode);
public interface ILegacyInvoices
{
    Task<LegacyInvoice> FetchAsync(int id, CancellationToken ct);
}
public interface IInvoiceStatus
{
    Task<InvoiceStatus> GetAsync(int id, CancellationToken ct);
}
public sealed class InvoiceAdapter(ILegacyInvoices legacy) : IInvoiceStatus
{
    public async Task<InvoiceStatus> GetAsync(int id, CancellationToken ct)
    {
        var dto = await legacy.FetchAsync(id, ct); // (1)
        return dto.StateCode switch // (2)
        {
            "X9" => new(true),
            "N0" => new(false),
            _ => throw new InvalidDataException("Unknown invoice state")
        };
    }
}
🧾 Code walkthrough
  1. (1) ILegacyInvoices is the transport-wrapper seam; its implementation knows the actual proxy and cancellation capabilities.
  2. (2) Map known values into our contract and reject unknown states rather than silently treating them as unpaid. Consumers depend only on IInvoiceStatus.

Cover mapping with contract tests and log correlation IDs without sensitive payloads. This boundary adds code; avoid layers of forwarding when models already match and the integration is simple.

#Moving a WCF server: CoreWCF or gRPC?

Changing the target framework does not bring the original server to 10. If existing clients must remain, evaluate CoreWCF and test required bindings, security and behaviors. If all consumers can change, is an option for a new contract, requiring regenerated clients and a different wire protocol.

Older approachModern pathCost
WCF SOAP serverCoreWCF with a compatible contract where possibleVerify feature support; not a universal drop-in
Internal contractgRPC and ProtobufClient, hosting and observability changes
SOAP consumerSystem.ServiceModel client packagesCompatibility depends on the required binding

Prototype one operation with the hardest security requirement first. Separate runtime migration from wholesale contract redesign. A SOAP facade can remain over a new implementation while clients migrate.

#Strangler fig with YARP: migrate route by route

A complete rewrite delays feedback and concentrates risk. Put one entry point in front of old and new systems, then move complete features gradually. Here serves migrated routes locally, and forwards everything else to the legacy application.

// Package: Yarp.ReverseProxy; configuration shown below.
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); // (1)
var app = builder.Build();
app.MapGet("/products/{id:int}", (int id) => new { Id = id }); // (2)
app.MapReverseProxy(); // (3)
app.Run();
{
  "ReverseProxy": {
    "Routes": {
      "fallback": {
        "ClusterId": "legacy",
        "Order": 1000,
        "Match": { "Path": "{**catch-all}" }
      }
    },
    "Clusters": {
      "legacy": {
        "Destinations": {
          "primary": { "Address": "https://legacy.example/" }
        }
      }
    }
  }
}
🧾 Code walkthrough
  1. (1) Routes and destinations come from configuration.
  2. (2) This illustrative migrated route would call an application service in a real application.
  3. (3) A lower-priority fallback forwards remaining requests. Endpoint selection uses route matching and Order, not simply source-code order.

Define data write ownership, compatible schemas, cookies, sessions, authentication and rollback. Shadow reads can compare behavior; shadow writes must not charge twice. Monitor remaining legacy traffic and define retirement criteria.

Source: Incremental ASP.NET migration.

#A verifiable .NET Framework to .NET 10 migration

  1. Inventory dependencies, Windows-specific and integration contracts; baseline failures and performance.
  2. Capture current behavior with characterization tests, and choose a valuable small slice with rollback.
  3. Upgrade leaf libraries required by that slice first. netstandard2.0 bridges shared libraries to Framework; it is not a runtime. Framework 4.7.2 or later is preferable in practice to avoid older compatibility problems. Framework does not support netstandard2.1.
  4. Replace hosting and configuration boundaries, then validate end-to-end behavior, deployment and load before shifting traffic.
Older approachModern pathVerify
System.Web , HttpContext and Request lifecycle, sessions and authentication; temporary adapters where needed
EF6 when justified translation, tracking and migrations; not a package rename
web.config application settingsappsettings.json, environment variables and OptionsKeep secrets outside files; web.config may remain for hosting
Framework-only librarynetstandard2.0 or multi-targetingAll dependencies must be compatible; new application code targets net10.0
Upgrade Assistant helped automate mechanical changes but is now deprecated. Microsoft directs users to GitHub Copilot app modernization; generated changes still require review and tests. Tooling does not replace migration design. Sources: Migration overview and .NET Standard compatibility.

#Characterization tests and seams

Before refactoring, establish what the code actually does, including surprising behavior. A characterization test captures current behavior; a seam lets you substitute a dependency such as a clock or without changing the business logic.

// TimeProvider is the seam; this example captures an existing date rule.
public sealed class InvoiceDates(TimeProvider clock)
{
    public DateOnly Today() =>
        DateOnly.FromDateTime(clock.GetUtcNow().UtcDateTime); // (1)
}
public sealed class FixedClock : TimeProvider
{
    public override DateTimeOffset GetUtcNow() =>
        new(2026, 10, 22, 0, 0, 0, TimeSpan.Zero);
}

[Fact]
public void PreservesUtcInvoiceDate()
{
    var dates = new InvoiceDates(new FixedClock()); // (2)
    Assert.Equal(new DateOnly(2026, 10, 22), dates.Today());
}
🧾 Code walkthrough
  1. (1) Preserve the current rule rather than changing to local time during refactoring.
  2. (2) The xUnit test uses a fixed clock, avoiding midnight failures. For legacy code, compare actual output before and after inserting the seam.

For golden masters, normalize only nondeterministic fields such as generated IDs, never meaningful differences. Record incorrect behavior explicitly and fix it as a separate agreed change; characterization does not permanently endorse a bug.

#Reliable integration: outbox, idempotency and reconciliation

Saving an order and sending a message are separate operations: the process can crash between them. Store the order and an row in the same local , then dispatch later. Delivery can repeat if the process crashes after sending but before marking success.

(1) Transaction: save Order + Outbox(MessageId, OrderId, Version, Payload)
(2) Dispatcher: lease pending row, send with stable MessageId, record success
(3) Receiver transaction: unique Inbox(MessageId) + apply business update
(4) Reconciliation: compare by business key, version, amount and currency
🧾 Code walkthrough
  1. (1) Both writes commit or roll back in one database transaction.
  2. (2) A lease reduces worker contention but cannot prevent duplicates; apply bounded backoff and jitter only to transient failures.
  3. (3) A unique constraint and the effect in one transaction prevent duplicate application; external payments need the same provider key.
  4. (4) Compare actual state and safely repair or alert. Preserve ordering per order where required, not an unnecessary global order.

An outbox provides neither exactly-once delivery nor a distributed transaction. If the partner cannot deduplicate, a timeout is an unknown outcome: query status or reconcile rather than retry blindly. Monitor oldest-message age, retries and stuck messages.

#CSV, SFTP and batch processing

Not every partner exposes an . Agree a file contract covering encoding, delimiters, quoted fields, version, time zone, business keys, row counts and checksums. transfers over ; it differs from FTPS, which uses .

  1. Verify the SSH host key, upload under a temporary name, then use an agreed rename or completion marker.
  2. Register the file ID and checksum, stage and validate records, and quarantine rejected rows with reasons and line numbers.
  3. Apply small batches with unique business keys and durable checkpoints. Record progress after commit; a crash before checkpointing requires safe replay.
  4. Return accepted and rejected counts and reconcile totals. Specify partial acceptance versus whole-file atomicity.
File: invoices-20261022-v1.csv
FileId: stable partner batch identifier
Staging: FileId + RowNumber + BusinessKey + ValidationStatus
Checkpoint: FileId + LastCommittedBatch
Result: accepted count + rejected count + totals by currency

Do not treat Split(',') as a general parser: quoted fields can contain commas and newlines. Streaming limits loaded rows, but a HashSet of every key may still exhaust memory. Use database staging or external sorting when needed.

BackgroundService hosts a worker but is not a durable scheduler by itself. Hangfire, Quartz.NET or an external scheduler can manage schedules according to their configuration, but jobs still need . Define overlap protection across instances, misfires, daylight saving behavior, cancellation, retries and missed-run alerts. Do not launch critical jobs with Task.Run inside requests.

Interview questions

Check your reasoning

Why does this choice fit? When would it fail? What changes under higher load or a dependency failure?

26.1What are SOAP, WSDL and XSD?Junior
Model answer

I separate the message protocol from its description. defines an envelope with a body, optional headers and faults. describes the service operations, message contracts, bindings and endpoints, while defines the XML types those messages use. I usually generate a typed client from the WSDL rather than build envelopes manually. Before integrating, I check the SOAP version, binding and security requirements because a generated proxy alone does not guarantee interoperability.

26.2Why do XML namespaces matter?Junior
Model answer

I treat an element's identity as its namespace plus local name. Two elements called Invoice can represent different contracts if their namespace URIs differ. Conversely, changing a prefix from a to b does not change meaning when both map to the same URI. When debugging , I compare qualified names and schemas rather than just the visible tag spelling. I use XML or generated serializers so namespace handling and escaping are explicit.

26.3How do you consume a SOAP service in .NET 10?Mid
Model answer

I generate a proxy from a trusted using dotnet-svcutil or Visual Studio's Web Service Reference. The generated code uses System.ServiceModel client packages. I inspect the generated operations and configure the appropriate binding, endpoint, authentication, timeouts and message limits. I keep those types behind an adapter implementing our own interface. I also test faults and timeout behavior, and close healthy channels or abort failed ones instead of reusing a faulted client.

26.4Does modern .NET support WCF servers?Mid
Model answer

I distinguish client packages from the original WCF server runtime. Modern can consume supported contracts with System.ServiceModel clients, but it does not host the original Framework WCF server. I evaluate CoreWCF when existing SOAP clients must remain compatible, checking bindings, security and behaviors individually. If we control all consumers, I may design a contract instead. That changes the wire protocol and clients, so it is a separate migration decision.

26.5What is an anti-corruption layer?Mid
Model answer

I use an anti-corruption layer to keep a legacy or partner model from spreading into our domain. It contains adapters and translation rules for data, behavior and failures. For example, an external invoice status code becomes our own meaningful status, and unknown codes are handled explicitly. This gives consumers a stable interface and keeps partner-specific changes localized. I test the mapping against real contract examples, while avoiding unnecessary abstraction when the integration is already simple.

26.6What is the strangler fig pattern?Mid
Model answer

I migrate one capability at a time while old and new implementations coexist behind a routing boundary. A proxy sends migrated routes to the new application and leaves the rest on the legacy system. Each slice has tests, observability and a rollback plan. I also define data ownership and cross-cutting concerns such as authentication before moving traffic. The pattern reduces the size of each change, but coexistence costs money and complexity, so I track explicit legacy retirement criteria.

26.7What would your .NET Framework migration plan look like?Senior
Model answer

I start with dependency and platform assessment, production baselines and characterization tests. Then I choose a small business slice and upgrade its leaf libraries, using netstandard2.0 or multi-targeting only where sharing with Framework is needed. I replace System.Web boundaries, evaluate hosting, and validate query behavior and configuration changes separately. I use migration tooling for mechanical edits, review its output, and move traffic gradually with rollback. A successful build is only the beginning of validation.

26.8What is a characterization test?Junior
Model answer

I write characterization tests to capture what existing code currently does before changing its structure. They are useful when requirements are incomplete or behavior has evolved over years. I select representative inputs, record outputs and side effects, and make unstable values deterministic where possible. The tests may preserve a known defect temporarily, so I label that explicitly and change it in a separate agreed fix. Their purpose is to expose accidental behavior changes during modernization.

26.9What is a seam in legacy code?Mid
Model answer

I think of a seam as a place where I can change a dependency's behavior without rewriting the business logic being tested. Constructor injection for a clock, a file reader or a payment client is a common example. I introduce the smallest useful seam, preserve behavior with characterization tests, and supply a deterministic test implementation. I avoid a broad redesign just to add one test, because that increases the unverified change before the safety net exists.

26.10Why is migrating EF6 to EF Core more than renaming packages?Senior
Model answer

I treat as a different implementation rather than an EF6 package upgrade. Query translation, relationship conventions, loading, tracking and migrations can differ, so code that compiles may behave differently or issue more queries. I compare representative results, generated and performance against a real database. I also plan evolution independently so both application versions remain compatible. Depending on dependencies, I may postpone the ORM migration to avoid combining too many risks.

26.11How do you prevent lost integration messages?Mid
Model answer

I save the business change and an record in the same local database . A dispatcher reads pending records, sends them with a stable message identifier and marks them complete after acknowledgment. If it crashes after sending, the record can be sent again, so the receiver must deduplicate and apply its effect atomically where possible. I monitor backlog age and failed attempts, and keep a repair path for messages that cannot progress automatically.

26.12A payment call timed out. Should you retry it?Senior
Model answer

I do not interpret a timeout as proof that the payment failed. The provider may have completed the charge before the response was lost. I reuse the original key if the provider supports it and understands its retention rules, or query the payment by a stable business reference. Without either capability, I record an unknown outcome and reconcile before resending. A generic retry policy can otherwise turn a temporary network problem into a duplicate charge.

26.13Why do integrations need reconciliation if they already use an outbox?Senior
Model answer

I use an to protect local event recording, but it cannot prove that both systems represent the same final business state. Mapping defects, manual edits, rejected records and partner-side failures can still create differences. Reconciliation compares stable business keys, versions, statuses and monetary totals after allowing for expected delay. It classifies discrepancies and either repairs them idempotently or raises an actionable exception. I audit repairs so reconciliation does not become an invisible source of new changes.

26.14What is the difference between SFTP and FTPS?Junior
Model answer

I check the actual protocol rather than treating secure file transfer as one interchangeable option. is a file-transfer protocol over , while FTPS is FTP secured with . They use different clients and server configurations. For SFTP I verify the server's host key and use managed credentials or keys. Encryption protects the connection, but I still need file completeness checks, duplicate detection and acknowledgments to know whether the business import succeeded.

26.15How would you import a large CSV safely?Mid
Model answer

I agree the , encoding, quoting rules and file completion protocol first. I stream records through a proper parser into staging, validate them, and apply bounded batches with stable business keys. I track file identity and durable progress so replay after a crash is safe. Invalid records go to a quarantine with clear reasons. Finally I return accepted and rejected counts and reconcile totals, with an explicit policy for partial acceptance versus rejecting the entire file.

26.16Is BackgroundService enough for reliable scheduled jobs?Mid
Model answer

I use BackgroundService as a hosting mechanism, not as a guarantee of durable scheduling. In-memory timers and progress disappear on restart, and multiple application replicas may start the same job. Depending on the requirement, I use a scheduler or durable job store and define overlap protection, retry policy, missed-run behavior and time-zone rules. The job itself remains and checkpointed, because even a durable scheduler can deliver work more than once after failures.

26.17How would you roll back a partially migrated feature?Senior
Model answer

I design rollback before routing production traffic. The old application must still understand data written by the new version, so I use compatible changes and delay destructive cleanup. I define one writer for each business operation and verify authentication and session behavior through the proxy. If metrics or business checks regress, I can route the slice back while preserving completed writes. For irreversible changes, I need a recovery or compensation plan rather than pretending a route switch reverses them.

26.18How do you handle ordering and poison messages?Mid
Model answer

I first identify which operations actually require ordering, usually changes for the same order rather than every message globally. I use an entity key and version to detect stale updates or gaps, and define whether a failure blocks later changes for that entity. Transient failures get bounded retries; invalid payloads or mapping failures are quarantined with diagnostics and an owner. After fixing the cause, replay uses the original identifiers so recovery does not create duplicate business effects.

Test yourself

Quiz answers and completed-topic marks are saved in this browser; they do not sync across devices.

Score: 0 / 0

What does WSDL primarily describe?

describes the service contract; defines data types.

Which statement about .NET 10 and WCF is correct?

Evaluate CoreWCF for server migration and verify each required feature.

A payment request times out. What do you know?

The request may have executed while its response was lost; query status or reuse its key.

What belongs inside an anti-corruption layer?

The boundary prevents partner concepts from spreading into the domain.

An outbox dispatcher crashes after sending but before marking success. What is likely?

The receiver needs deduplication atomic with the effect.

Which library target can bridge suitable Framework and .NET 10 code?

Framework does not support netstandard2.1; dependencies must also be compatible.

What is a characterization test's immediate purpose?

It exposes behavior changes during refactoring, including changes to known defects.

What indicates a CSV file is ready for import?

A filename alone does not prove completion; use an agreed completion protocol.

What must be considered before routing a migrated feature back to the old app?

Rollback depends on data and compatibility, not just traffic routing.

What does streaming CSV processing guarantee by itself?

Lookup state can still grow despite streaming; use staging when needed.