Forward Deployed Engineer course · lesson 12 of 24
- 1What Is a Forward Deployed Engineer?
- 2A Day in the Life of a Forward Deployed Engineer
- 3FDE Career Paths, Tiers, and Interview Gates
- 4Build Your Forward Deployed Engineer Skill Map
- 5Requirements Gathering with a Customer
- 6Discovery, Scoping, and Sequencing Customer Work
- 7Audit Customer Data Before You Build
- 8Enterprise Workflow, Business Value, and ROI
- 9Choosing an AI Approach for a Customer
- 10Build a Working Prototype Fast
- 11Integration Engineering and the Last Mile
- 12Working Safely with Legacy Systems
- 13Data Pipelines for AI Features
- 14Evals as Acceptance Criteria
- 15Debugging Agent Logic and Observability
- 16Deploying in the Customer's Cloud
- 17Cost, Latency and Inference Choices
- 18Security, Privacy and AI Governance
- 19Stakeholders and Communication
- 20Technical Writing for Handoff
- 21UAT, Go-Live, and Day 2 Operations
- 22Executive Reporting and Proving ROI
- 23The Product Feedback Loop
- 24Capstone: Run an Engagement End to End
After this lesson, you will be able to learn an unfamiliar system's behavior, put a small adapter around its interface, and plan changes so a failed request is less likely to create duplicate work. You will build and test a Python adapter with bounded retries, an idempotent write key, and a dry-run mode. NorthPort Freight is fictional. Its order records and legacy interface are synthetic examples, not details from a real customer system.
A legacy system is an existing system that a team must keep using even when its code, documentation, interface, or support model is difficult to change. Age alone does not tell you whether a system is risky. A newer service can be poorly understood; a decades-old program can have a stable and well-documented interface. The practical question is what the system does today, what depends on it, and how safely you can learn more.
NorthPort's fictional dispatch team uses an old ordering application. Dispatchers know that a short status code means an order is held, but no current document explains every code. Other jobs read its nightly export. The system still supports work people need to complete. An FDE who changes the field or its meaning without checking those dependencies can break a report, a handoff, or a user's routine.

Read before changing
Start with people and evidence the customer already has. Ask who operates the system, who understands the workflow, what must stay available, and what changes are already scheduled. Look for runbooks, interface descriptions, support tickets, release notes, batch schedules, and examples of real interactions that the owner has approved you to inspect. A document can be outdated, so compare it with observed behavior and ask about mismatches.
Write down what you can observe without changing state. Identify inputs, outputs, timing, error responses, side effects, file locations, and downstream users. A batch file is a group of records transferred together, often on a schedule. Find out whether the file is overwritten or appended, how its name signals the date, what encoding it uses, whether an empty file means "no changes" or "export failed," and who notices a missed run. A screen may reveal behavior that no document captures. If you have customer approval to inspect it, record the steps, fields, and decisions a user makes. Do not automate clicks just because the screen is visible. A label can move, a dialog can appear, or a session can expire. Screen automation can be a last resort when the system has no supported interface and a human owner agrees to the failure and maintenance plan.
Direct database access also needs care. A database can contain fields whose meaning is unclear, and a direct write can bypass checks performed by the application. Prefer a supported service or export. If the owner says a database connection is the only route, agree on a read-only inspection first, confirm which tables and fields are in scope, and ask the database owner about transactions, constraints, and backups before proposing writes. Never infer that a column is safe to edit from its name alone.
Record unknowns explicitly. For example: "We saw the dispatch screen change the status after approval, but we have not confirmed whether the nightly job reads that field." That sentence tells a reviewer what was observed and what still needs an owner. Avoid turning an assumption into a requirement by repeating it in a ticket or diagram.
Capture behavior with characterization tests
A characterization test records what the existing system does for an input so you can spot an unintended change later. It answers "what happens now?" A specification test checks "what should happen?" You may not know the intended behavior yet. Start by capturing a small set of representative, approved inputs and outputs, then ask the workflow owner whether each result is expected.
Suppose the legacy export contains ORDER_NO=NP-100 and DSP_ST=O. A test can record that the current parser reads this row as order NP-100 with status open, if that mapping has been verified with the customer. Add cases for a held order, an empty status, an unknown code, and a missing order number. Do not guess what a code means and then write a test that makes the guess look official.
Tests do not prove that every behavior is correct. They make the captured examples repeatable and give you a way to notice when the adapter changes them. Keep the observed input, output, source, date, and confirming owner with the test notes. If the system changes and a test fails, inspect the difference with the owner before updating the expectation. A failing test may reveal a regression or a legitimate change.
For unfamiliar code, keep the first change narrow. Michael Feathers's paper on working effectively with legacy code calls the early tests that preserve observed behavior test coverings. He recommends building a safety net around the part you need to change before refactoring or adding new behavior. You do not need to document the entire application before making progress.
Put a translation layer at the edge
An adapter is a small piece of code that lets one interface work with another. Here, it keeps old field names and codes out of the newer workflow. The new code can ask for an Order with a readable status. The adapter translates that request to the legacy format and converts the returned record back into the workflow's terms.
An anti-corruption layer is a boundary that translates between one system's model and another, so legacy concepts do not spread through the new code. The Legacy Mimic pattern describes an adapter or similar layer that lets a new system use the old system's existing interface while translating between their models. Keep the layer small enough that a reviewer can see its field mappings, allowed operations, and error handling.
The adapter is also a useful place to enforce a narrow contract. It can reject an unknown status, normalize a blank field, validate an operation identifier, and turn a legacy error into a clear application error. It should not quietly "fix" data by inventing a value. If a legacy record cannot be interpreted, return an explicit error and send the case to someone who can resolve it.
The adapter depends on a narrow protocol: it states which methods another object must provide, without requiring that object to inherit from a particular base class. LegacyPort is the seam between the adapter and a real system. An in-memory substitute keeps the example and tests away from networks, credentials, customer data, and real legacy databases.
The synthetic legacy record uses ORDER_NO, DSP_ST, and DISPATCH_DATE. The example maps O to open and H to held; these codes are invented for this lesson. Confirm every field and code with a system owner before making a real adapter.
legacy_adapter.py
1from dataclasses import dataclass
2from typing import Callable, Mapping, Protocol, TypeVar
3import time
4
5
6class LegacyError(Exception):
7 pass
8
9
10class LegacyTemporaryError(LegacyError):
11 pass
12
13
14class LegacyRecordNotFound(LegacyError):
15 pass
16
17
18class LegacyPermanentError(LegacyError):
19 pass
20
21
22class LegacyPort(Protocol):
23 def fetch_order(self, order_reference: str) -> Mapping[str, str]:
24 ...
25
26 def mark_reviewed(self, order_reference: str, operation_id: str) -> None:
27 ...
28
29
30@dataclass(frozen=True)
31class Order:
32 reference: str
33 status: str
34 dispatch_date: str
35
36
37@dataclass(frozen=True)
38class WriteResult:
39 status: str
40 attempts: int
41 order_reference: str
42
43
44T = TypeVar("T")
45
46
47class LegacyAdapter:
48 STATUS_CODES = {"O": "open", "H": "held"}
49
50 def __init__(
51 self,
52 port: LegacyPort,
53 *,
54 dry_run: bool = True,
55 max_attempts: int = 3,
56 base_delay_seconds: float = 0.1,
57 sleep: Callable[[float], None] = time.sleep,
58 ) -> None:
59 if max_attempts < 1:
60 raise ValueError("max_attempts must be at least 1")
61 if base_delay_seconds < 0:
62 raise ValueError("base_delay_seconds cannot be negative")
63 self.port = port
64 self.dry_run = dry_run
65 self.max_attempts = max_attempts
66 self.base_delay_seconds = base_delay_seconds
67 self.sleep = sleep
68
69 def get_order(self, order_reference: str) -> Order:
70 self._validate_identifier(order_reference, "order_reference")
71 raw, _ = self._with_retries(
72 lambda: self.port.fetch_order(order_reference)
73 )
74 legacy_reference = raw.get("ORDER_NO", "").strip()
75 status_code = raw.get("DSP_ST", "").strip()
76 dispatch_date = raw.get("DISPATCH_DATE", "").strip()
77 if not legacy_reference or not dispatch_date:
78 raise LegacyPermanentError("legacy order is missing required fields")
79 if status_code not in self.STATUS_CODES:
80 raise LegacyPermanentError(
81 f"unknown legacy status code: {status_code or '<blank>'}"
82 )
83 return Order(
84 reference=legacy_reference,
85 status=self.STATUS_CODES[status_code],
86 dispatch_date=dispatch_date,
87 )
88
89 def mark_reviewed(
90 self, order_reference: str, operation_id: str
91 ) -> WriteResult:
92 self._validate_identifier(order_reference, "order_reference")
93 self._validate_identifier(operation_id, "operation_id")
94 if self.dry_run:
95 return WriteResult("dry_run", 0, order_reference)
96 _, attempts = self._with_retries(
97 lambda: self.port.mark_reviewed(order_reference, operation_id)
98 )
99 return WriteResult("applied", attempts, order_reference)
100
101 def _with_retries(self, operation: Callable[[], T]) -> tuple[T, int]:
102 for attempt in range(1, self.max_attempts + 1):
103 try:
104 return operation(), attempt
105 except LegacyTemporaryError:
106 if attempt == self.max_attempts:
107 raise
108 delay = self.base_delay_seconds * (2 ** (attempt - 1))
109 self.sleep(delay)
110 raise AssertionError("retry loop ended unexpectedly")
111
112 @staticmethod
113 def _validate_identifier(value: str, name: str) -> None:
114 if not value or not value.strip():
115 raise ValueError(f"{name} cannot be blank")max_attempts counts the first call. With three attempts, the adapter can call once and retry twice. The delay grows between attempts, and the injected sleep function makes that schedule testable without waiting during tests. Only LegacyTemporaryError is retried. A missing record, invalid field, or permission error should not be retried as though waiting will fix it.
The port's mark_reviewed contract requires the legacy side to recognize the same operation_id if a request is repeated. If the first call applies the change but its response is lost, the retry must return the result for that same operation instead of applying a second effect. A random new identifier on each retry defeats that protection. The HTTP Semantics standard defines idempotent request effects and cautions against automatically retrying a non-idempotent request without a way to know it is safe. A local wrapper cannot create durable idempotency if the remote system cannot store or check the operation key.
Try the adapter with a synthetic system
This example provides the LegacyPort methods with a tiny in-memory implementation. It simulates a temporary failure after saving a write, which resembles the ambiguous case where a server commits a change and the reply is lost. The operation ID lets a retry retrieve the prior result. This behavior belongs to the synthetic store, not to any real NorthPort system.
The adapter defaults to dry_run=True. In that mode, mark_reviewed validates its identifiers and returns a preview without calling the write method. Reading an order is still allowed. A dry run can help inspect the intended action, but it is not an approval and does not prove a production write will succeed.
example.py
1from legacy_adapter import (
2 LegacyAdapter,
3 LegacyPermanentError,
4 LegacyRecordNotFound,
5 LegacyTemporaryError,
6)
7
8
9class DemoLegacyStore:
10 def __init__(self) -> None:
11 self.records = {
12 "NP-100": {
13 "ORDER_NO": "NP-100",
14 "DSP_ST": "O",
15 "DISPATCH_DATE": "2026-11-05",
16 }
17 }
18 self.completed_operations: dict[str, str] = {}
19 self.write_effects: list[str] = []
20 self.fail_after_save = 1
21
22 def fetch_order(self, order_reference: str) -> dict[str, str]:
23 try:
24 return self.records[order_reference].copy()
25 except KeyError as error:
26 raise LegacyRecordNotFound(order_reference) from error
27
28 def mark_reviewed(self, order_reference: str, operation_id: str) -> None:
29 if operation_id in self.completed_operations:
30 if self.completed_operations[operation_id] != order_reference:
31 raise LegacyPermanentError("operation ID belongs to another order")
32 return
33 if order_reference not in self.records:
34 raise LegacyRecordNotFound(order_reference)
35 self.completed_operations[operation_id] = order_reference
36 self.write_effects.append(operation_id)
37 self.records[order_reference]["REVIEWED"] = "Y"
38 if self.fail_after_save:
39 self.fail_after_save -= 1
40 raise LegacyTemporaryError("reply lost after save")
41
42
43store = DemoLegacyStore()
44preview = LegacyAdapter(store).mark_reviewed("NP-100", "review-NP-100-1")
45print(preview)
46
47adapter = LegacyAdapter(
48 store,
49 dry_run=False,
50 base_delay_seconds=0,
51 sleep=lambda _: None,
52)
53print(adapter.mark_reviewed("NP-100", "review-NP-100-1"))
54print(adapter.get_order("NP-100"))
55print("saved effects:", len(store.write_effects))Run it with python3 example.py. The output should show a dry-run result, an applied result after two attempts, an Order with status open, and saved effects: 1. The second attempt returns the result for the same operation ID, so the synthetic store records one effect. The zero delay keeps the sample quick; in a real integration, choose limits and delays with the system owner and the provider's documented behavior.
The code is intentionally a seam, not a customer-ready connector. It does not open a socket, parse a batch file, authenticate, or implement a vendor protocol. A production LegacyPort would translate these methods to the customer's supported interface. It would need to map real error responses carefully: only a documented transient failure should become LegacyTemporaryError, and the target must support an idempotency key or another verified deduplication mechanism before retrying writes.
Add tests around the seam
The tests below use a separate fake store so each behavior can be controlled. A fake is a small test object that stands in for a dependency and returns known results. It is useful for checking adapter logic; it cannot show that a real legacy interface has the same behavior. Keep a separate customer-approved test for the real interface when access is available.
The first tests capture the synthetic field mapping and reject a code the adapter does not know. The write tests check the default dry run, validation, temporary failure before a write, an ambiguous reply after a write, permanent errors, and exhausted attempts. The sleep recorder verifies the delay schedule without pausing the test run.
test_legacy_adapter.py
1import unittest
2
3from legacy_adapter import (
4 LegacyAdapter,
5 LegacyPermanentError,
6 LegacyRecordNotFound,
7 LegacyTemporaryError,
8)
9
10
11class FakeLegacyStore:
12 def __init__(self) -> None:
13 self.records = {
14 "NP-100": {
15 "ORDER_NO": "NP-100",
16 "DSP_ST": "O",
17 "DISPATCH_DATE": "2026-11-05",
18 }
19 }
20 self.completed_operations: dict[str, str] = {}
21 self.write_effects: list[str] = []
22 self.write_attempts = 0
23 self.fail_before_save = 0
24 self.fail_after_save = 0
25 self.permanent_failure = False
26
27 def fetch_order(self, order_reference: str) -> dict[str, str]:
28 try:
29 return self.records[order_reference].copy()
30 except KeyError as error:
31 raise LegacyRecordNotFound(order_reference) from error
32
33 def mark_reviewed(self, order_reference: str, operation_id: str) -> None:
34 self.write_attempts += 1
35 if self.permanent_failure:
36 raise LegacyPermanentError("write is not allowed")
37 if operation_id in self.completed_operations:
38 if self.completed_operations[operation_id] != order_reference:
39 raise LegacyPermanentError("operation ID belongs to another order")
40 return
41 if self.fail_before_save:
42 self.fail_before_save -= 1
43 raise LegacyTemporaryError("system is busy")
44 if order_reference not in self.records:
45 raise LegacyRecordNotFound(order_reference)
46 self.completed_operations[operation_id] = order_reference
47 self.write_effects.append(operation_id)
48 self.records[order_reference]["REVIEWED"] = "Y"
49 if self.fail_after_save:
50 self.fail_after_save -= 1
51 raise LegacyTemporaryError("reply was lost")
52
53
54class LegacyAdapterTests(unittest.TestCase):
55 def setUp(self) -> None:
56 self.store = FakeLegacyStore()
57 self.delays: list[float] = []
58
59 def adapter(self, **options: object) -> LegacyAdapter:
60 options.setdefault("base_delay_seconds", 0.1)
61 return LegacyAdapter(
62 self.store,
63 sleep=self.delays.append,
64 **options,
65 )
66
67 def test_maps_legacy_fields_to_order(self) -> None:
68 order = self.adapter().get_order("NP-100")
69 self.assertEqual(order.reference, "NP-100")
70 self.assertEqual(order.status, "open")
71 self.assertEqual(order.dispatch_date, "2026-11-05")
72
73 def test_missing_order_raises_not_found(self) -> None:
74 with self.assertRaises(LegacyRecordNotFound):
75 self.adapter().get_order("NP-404")
76
77 def test_unknown_status_is_rejected(self) -> None:
78 self.store.records["NP-100"]["DSP_ST"] = "?"
79 with self.assertRaises(LegacyPermanentError):
80 self.adapter().get_order("NP-100")
81
82 def test_dry_run_does_not_write(self) -> None:
83 result = self.adapter().mark_reviewed("NP-100", "op-1")
84 self.assertEqual(result.status, "dry_run")
85 self.assertEqual(result.attempts, 0)
86 self.assertEqual(self.store.write_attempts, 0)
87
88 def test_blank_operation_id_is_rejected(self) -> None:
89 with self.assertRaises(ValueError):
90 self.adapter(dry_run=False).mark_reviewed("NP-100", " ")
91 self.assertEqual(self.store.write_attempts, 0)
92
93 def test_temporary_failure_retries_with_bounded_delays(self) -> None:
94 self.store.fail_before_save = 2
95 result = self.adapter(dry_run=False).mark_reviewed("NP-100", "op-2")
96 self.assertEqual(result.attempts, 3)
97 self.assertEqual(self.delays, [0.1, 0.2])
98 self.assertEqual(self.store.write_effects, ["op-2"])
99
100 def test_ambiguous_reply_reuses_key_without_duplicate_effect(self) -> None:
101 self.store.fail_after_save = 1
102 result = self.adapter(dry_run=False).mark_reviewed("NP-100", "op-3")
103 self.assertEqual(result.attempts, 2)
104 self.assertEqual(self.store.write_attempts, 2)
105 self.assertEqual(self.store.write_effects, ["op-3"])
106
107 def test_operation_id_cannot_be_reused_for_another_order(self) -> None:
108 self.store.records["NP-101"] = {
109 "ORDER_NO": "NP-101",
110 "DSP_ST": "O",
111 "DISPATCH_DATE": "2026-11-06",
112 }
113 adapter = self.adapter(dry_run=False)
114 adapter.mark_reviewed("NP-100", "op-4")
115 with self.assertRaises(LegacyPermanentError):
116 adapter.mark_reviewed("NP-101", "op-4")
117 self.assertEqual(self.store.write_effects, ["op-4"])
118
119 def test_permanent_error_is_not_retried(self) -> None:
120 self.store.permanent_failure = True
121 with self.assertRaises(LegacyPermanentError):
122 self.adapter(dry_run=False).mark_reviewed("NP-100", "op-5")
123 self.assertEqual(self.store.write_attempts, 1)
124 self.assertEqual(self.delays, [])
125
126 def test_retries_stop_at_attempt_limit(self) -> None:
127 self.store.fail_before_save = 5
128 with self.assertRaises(LegacyTemporaryError):
129 self.adapter(dry_run=False, max_attempts=3).mark_reviewed(
130 "NP-100", "op-6"
131 )
132 self.assertEqual(self.store.write_attempts, 3)
133 self.assertEqual(self.delays, [0.1, 0.2])
134
135 def test_invalid_retry_policy_is_rejected(self) -> None:
136 with self.assertRaises(ValueError):
137 self.adapter(max_attempts=0)
138 with self.assertRaises(ValueError):
139 self.adapter(base_delay_seconds=-0.1)
140
141
142if __name__ == "__main__":
143 unittest.main()Copy the two code files and the test file into one empty folder, then run python3 -m unittest discover -s . -p 'test_*.py'. The expected result is Ran 11 tests followed by OK. The example can be checked separately with python3 example.py. Both commands use only the Python standard library. Python 3.10 or newer is required for the type syntax; the official Python unittest documentation explains the test case and runner used here.
A useful mutation check confirms the tests can catch a broken safeguard. In mark_reviewed, temporarily change if self.dry_run: to if False: and rerun the suite. test_dry_run_does_not_write should fail because the fake store receives a write. Restore the original condition and rerun all tests before relying on the result. A green test suite is evidence about these defined cases, not proof of production safety.
Choose a safe order of access
Begin with reads through a supported interface and compare the values with what a system owner expects. A shadow check sends a copy of an input through a new path while the existing system remains authoritative; the new path's result is recorded for comparison and does not control the user's workflow. Shadowing can increase load, so use a small sample and agree on limits before sending duplicate reads. Avoid shadow writes unless the target explicitly guarantees they cannot change business state.
After the read and comparison behavior is understood, use a dry run to show which write the adapter would make. Have the owner review the exact target and operation key. Then test one controlled write in an approved environment with a recovery plan. Increase scope only after the result, audit trail, and rollback procedure have been reviewed. A rollback for an external side effect may mean a compensating action or operator procedure, not an automatic reversal.

A direct screen integration or database connection belongs near the end of this sequence because it can couple your code to details the system owner may change. If there is no supported interface, document why the alternative is needed, who accepts its fragility, what will detect a break, and how access can be removed. If an owner cannot approve the route, keep the adapter at a fixture or test seam.
Keep the change small and reversible
When the goal is eventual replacement, avoid a single rewrite that moves every workflow at once. The strangler fig pattern replaces parts of an existing application gradually, with routing that sends selected work to the new implementation while other work stays on the old one. AWS Prescriptive Guidance describes this pattern for incremental migration of a monolith. Its examples assume access to the application and routing layer. If a customer does not control those pieces, the pattern may not be available to your team.
A smaller code-level migration can use branch by abstraction. The team places an interface between callers and the current implementation, then can provide a second implementation behind the same interface. Martin Fowler's description of branch by abstraction shows how an abstraction can support a gradual replacement. An FDE often does not own the legacy application or its callers, so use this as awareness for discussions with the product team rather than a promise that you can migrate the system yourself.
Keep a change log for the seam: the old behavior captured, mapping decisions, permissions, retry policy, operation key, tested failure cases, and current owner. Name the limits too. The code here has no durable store and no real provider. It assumes the old system can enforce idempotency by operation ID. If that capability is absent, disable write retries and agree how an operator will check an uncertain result before trying again.
Use this review sheet before the first connection:
| Area | Record before changing anything | Owner or evidence |
|---|---|---|
| Business use | Which user task depends on this system, and what must keep working? | |
| Observed behavior | Which approved inputs, outputs, schedules, error cases, and side effects were recorded? | |
| Data mapping | Which old fields and codes map to the new model? Who confirmed each meaning? | |
| Access path | Which supported API, event, or batch export can be used? If none, why is a screen or database route necessary? | |
| Read and shadow | What is the sample size, load limit, comparison rule, and stop condition? | |
| Write safety | Is the operation id stable across retries? Can the target deduplicate it? What happens after an unknown result? | |
| Dry run | What would change, and who reviewed the preview? | |
| Recovery | Who can pause the integration, inspect a partial result, and perform a compensating action? | |
| Change and support | Who owns the adapter, old interface, alerts, and future field changes? | |
| Exit | How can this seam be disabled or replaced without losing needed records? |
Mistakes beginners make
- Editing a field after reading its name. Check actual behavior and confirm its meaning with someone who owns the workflow.
- Testing only the result you wish the old system produced. First capture what it does now, then agree on any behavior that should change.
- Spreading legacy codes throughout the new application. Translate them at one adapter boundary and reject codes the adapter cannot explain.
- Retrying a write with a new request identifier each time. Keep one stable operation ID and confirm the target can deduplicate it.
- Treating dry-run output as proof the production write will work. It checks the proposed action without exercising the write path.
- Shadowing every request without checking capacity. Agree a small sample and a load limit with the system owner.
- Reaching directly into a database or automating a screen as the first option. Ask about a supported interface and document why a last-resort path is necessary.
- Planning a full replacement before isolating one workflow. Start with a small seam and make sure the existing path remains usable during the change.
Recap and try it yourself
Learn how the old path behaves before changing it. Capture a few approved examples in characterization tests, translate legacy fields at a narrow adapter, and keep reads separate from writes. A dry run previews an action. A shadow comparison observes a second path. Neither grants permission to change customer data. Retry only failures the adapter can classify, and only repeat a write when the target has a documented way to recognize the same operation.
Try it: use the included synthetic records to add one new status code. First write a test that captures the observed mapping, then update the adapter. Add a fake write that fails after it saves and verify that the same operation ID produces one effect. Finally, fill in the review sheet for one fictional NorthPort workflow and mark which facts would need confirmation from a real system owner.
Sources
Checked on 5 November 2026. The system and data in the code examples are fictional. The adapter and tests run locally without a customer system. They demonstrate the boundary and failure cases stated in the lesson, not a production integration.
- Legacy Mimic pattern, for translating between a new system's model and an existing interface through an isolating layer.
- Working Effectively with Legacy Code, for test coverings that capture current behavior and the practice of making narrow, tested changes.
- RFC 9110: HTTP Semantics, for idempotent request effects and the caution against retrying non-idempotent requests without evidence that it is safe.
- Python 3.12
unittestdocumentation, for the standard-library test cases and runner used in the example. Python version availability and supported syntax depend on the reader's installed interpreter. - Branch by Abstraction, for gradual replacement behind an abstraction.
- AWS Prescriptive Guidance on the Strangler Fig pattern, for incremental migration and the pattern's access and routing assumptions.