From c0f1e976f49fec1e8009098ad3a70bec820a5c45 Mon Sep 17 00:00:00 2001
From: Leandro Damascena <leandro.damascena@gmail.com>
Date: Tue, 20 Sep 2022 22:19:15 +0100
Subject: [PATCH 1/9] feat(v2/idempotency): Changing hash key computation

---
 .../utilities/idempotency/base.py             |  2 +-
 tests/functional/idempotency/conftest.py      | 14 +++++++---
 .../idempotency/test_idempotency.py           | 28 ++++++++++++-------
 tests/functional/idempotency/utils.py         | 12 ++++++--
 4 files changed, 39 insertions(+), 17 deletions(-)

diff --git a/aws_lambda_powertools/utilities/idempotency/base.py b/aws_lambda_powertools/utilities/idempotency/base.py
index ddd054daa14..9281c77109a 100644
--- a/aws_lambda_powertools/utilities/idempotency/base.py
+++ b/aws_lambda_powertools/utilities/idempotency/base.py
@@ -76,7 +76,7 @@ def __init__(
         self.fn_kwargs = function_kwargs
         self.config = config
 
-        persistence_store.configure(config, self.function.__name__)
+        persistence_store.configure(config, f"{self.function.__module__}.{self.function.__qualname__}")
         self.persistence_store = persistence_store
 
     def handle(self) -> Any:
diff --git a/tests/functional/idempotency/conftest.py b/tests/functional/idempotency/conftest.py
index b5cf79727b1..657a4b6bd13 100644
--- a/tests/functional/idempotency/conftest.py
+++ b/tests/functional/idempotency/conftest.py
@@ -172,18 +172,24 @@ def expected_params_put_item_with_validation(hashed_idempotency_key, hashed_vali
 
 
 @pytest.fixture
-def hashed_idempotency_key(lambda_apigw_event, default_jmespath, lambda_context):
+def hashed_idempotency_key(request, lambda_apigw_event, default_jmespath, lambda_context):
     compiled_jmespath = jmespath.compile(default_jmespath)
     data = compiled_jmespath.search(lambda_apigw_event)
-    return "test-func.lambda_handler#" + hash_idempotency_key(data)
+    return (
+        f"test-func.{request.function.__module__}.{request.function.__qualname__}.<locals>.lambda_handler#"
+        + hash_idempotency_key(data)
+    )
 
 
 @pytest.fixture
-def hashed_idempotency_key_with_envelope(lambda_apigw_event):
+def hashed_idempotency_key_with_envelope(request, lambda_apigw_event):
     event = extract_data_from_envelope(
         data=lambda_apigw_event, envelope=envelopes.API_GATEWAY_HTTP, jmespath_options={}
     )
-    return "test-func.lambda_handler#" + hash_idempotency_key(event)
+    return (
+        f"test-func.{request.function.__module__}.{request.function.__qualname__}.<locals>.lambda_handler#"
+        + hash_idempotency_key(event)
+    )
 
 
 @pytest.fixture
diff --git a/tests/functional/idempotency/test_idempotency.py b/tests/functional/idempotency/test_idempotency.py
index 97a9166efa0..f1e32cb5ebc 100644
--- a/tests/functional/idempotency/test_idempotency.py
+++ b/tests/functional/idempotency/test_idempotency.py
@@ -32,6 +32,7 @@
 from tests.functional.utils import json_serialize, load_event
 
 TABLE_NAME = "TEST_TABLE"
+TESTS_MODULE_PREFIX = "test-func.functional.idempotency.test_idempotency"
 
 
 def get_dataclasses_lib():
@@ -770,7 +771,7 @@ def lambda_handler(event, context):
 
 def test_idempotent_lambda_expires_in_progress_unavailable_remaining_time():
     mock_event = {"data": "value"}
-    idempotency_key = "test-func.function#" + hash_idempotency_key(mock_event)
+    idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_lambda_expires_in_progress_unavailable_remaining_time.<locals>.function#{hash_idempotency_key(mock_event)}"  # noqa E501
     persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key)
     expected_result = {"message": "Foo"}
 
@@ -1109,7 +1110,8 @@ def _delete_record(self, data_record: DataRecord) -> None:
 def test_idempotent_lambda_event_source(lambda_context):
     # Scenario to validate that we can use the event_source decorator before or after the idempotent decorator
     mock_event = load_event("apiGatewayProxyV2Event.json")
-    persistence_layer = MockPersistenceLayer("test-func.lambda_handler#" + hash_idempotency_key(mock_event))
+    idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_lambda_event_source.<locals>.lambda_handler#{hash_idempotency_key(mock_event)}"  # noqa E501
+    persistence_layer = MockPersistenceLayer(idempotency_key)
     expected_result = {"message": "Foo"}
 
     # GIVEN an event_source decorator
@@ -1129,7 +1131,9 @@ def lambda_handler(event, _):
 def test_idempotent_function():
     # Scenario to validate we can use idempotent_function with any function
     mock_event = {"data": "value"}
-    idempotency_key = "test-func.record_handler#" + hash_idempotency_key(mock_event)
+    idempotency_key = (
+        f"{TESTS_MODULE_PREFIX}.test_idempotent_function.<locals>.record_handler#{hash_idempotency_key(mock_event)}"
+    )
     persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key)
     expected_result = {"message": "Foo"}
 
@@ -1147,7 +1151,7 @@ def test_idempotent_function_arbitrary_args_kwargs():
     # Scenario to validate we can use idempotent_function with a function
     # with an arbitrary number of args and kwargs
     mock_event = {"data": "value"}
-    idempotency_key = "test-func.record_handler#" + hash_idempotency_key(mock_event)
+    idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_arbitrary_args_kwargs.<locals>.record_handler#{hash_idempotency_key(mock_event)}"  # noqa E501
     persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key)
     expected_result = {"message": "Foo"}
 
@@ -1163,7 +1167,7 @@ def record_handler(arg_one, arg_two, record, is_record):
 
 def test_idempotent_function_invalid_data_kwarg():
     mock_event = {"data": "value"}
-    idempotency_key = "test-func.record_handler#" + hash_idempotency_key(mock_event)
+    idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_invalid_data_kwarg.<locals>.record_handler#{hash_idempotency_key(mock_event)}"  # noqa E501
     persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key)
     expected_result = {"message": "Foo"}
     keyword_argument = "payload"
@@ -1200,7 +1204,7 @@ def record_handler(record):
 def test_idempotent_function_and_lambda_handler(lambda_context):
     # Scenario to validate we can use both idempotent_function and idempotent decorators
     mock_event = {"data": "value"}
-    idempotency_key = "test-func.record_handler#" + hash_idempotency_key(mock_event)
+    idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_and_lambda_handler.<locals>.record_handler#{hash_idempotency_key(mock_event)}"  # noqa E501
     persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key)
     expected_result = {"message": "Foo"}
 
@@ -1208,7 +1212,9 @@ def test_idempotent_function_and_lambda_handler(lambda_context):
     def record_handler(record):
         return expected_result
 
-    persistence_layer = MockPersistenceLayer("test-func.lambda_handler#" + hash_idempotency_key(mock_event))
+    persistence_layer = MockPersistenceLayer(
+        f"{TESTS_MODULE_PREFIX}.test_idempotent_function_and_lambda_handler.<locals>.lambda_handler#{hash_idempotency_key(mock_event)}"  # noqa E501
+    )
 
     @idempotent(persistence_store=persistence_layer)
     def lambda_handler(event, _):
@@ -1229,7 +1235,9 @@ def test_idempotent_data_sorting():
     # Scenario to validate same data in different order hashes to the same idempotency key
     data_one = {"data": "test message 1", "more_data": "more data 1"}
     data_two = {"more_data": "more data 1", "data": "test message 1"}
-    idempotency_key = "test-func.dummy#" + hash_idempotency_key(data_one)
+    idempotency_key = (
+        f"{TESTS_MODULE_PREFIX}.test_idempotent_data_sorting.<locals>.dummy#{hash_idempotency_key(data_one)}"
+    )
     # Assertion will happen in MockPersistenceLayer
     persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key)
 
@@ -1337,7 +1345,7 @@ def test_idempotent_function_dataclass_with_jmespath():
     dataclasses = get_dataclasses_lib()
     config = IdempotencyConfig(event_key_jmespath="transaction_id", use_local_cache=True)
     mock_event = {"customer_id": "fake", "transaction_id": "fake-id"}
-    idempotency_key = "test-func.collect_payment#" + hash_idempotency_key(mock_event["transaction_id"])
+    idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_dataclass_with_jmespath.<locals>.collect_payment#{hash_idempotency_key(mock_event['transaction_id'])}"  # noqa E501
     persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key)
 
     @dataclasses.dataclass
@@ -1362,7 +1370,7 @@ def test_idempotent_function_pydantic_with_jmespath():
     # GIVEN
     config = IdempotencyConfig(event_key_jmespath="transaction_id", use_local_cache=True)
     mock_event = {"customer_id": "fake", "transaction_id": "fake-id"}
-    idempotency_key = "test-func.collect_payment#" + hash_idempotency_key(mock_event["transaction_id"])
+    idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_pydantic_with_jmespath.<locals>.collect_payment#{hash_idempotency_key(mock_event['transaction_id'])}"  # noqa E501
     persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key)
 
     class Payment(BaseModel):
diff --git a/tests/functional/idempotency/utils.py b/tests/functional/idempotency/utils.py
index 797b696aba4..f9cdaf05d0a 100644
--- a/tests/functional/idempotency/utils.py
+++ b/tests/functional/idempotency/utils.py
@@ -14,9 +14,13 @@ def hash_idempotency_key(data: Any):
 def build_idempotency_put_item_stub(
     data: Dict,
     function_name: str = "test-func",
+    function_qualified_name: str = "test_idempotent_lambda_first_execution_event_mutation.<locals>",
+    module_name: str = "functional.idempotency.test_idempotency",
     handler_name: str = "lambda_handler",
 ) -> Dict:
-    idempotency_key_hash = f"{function_name}.{handler_name}#{hash_idempotency_key(data)}"
+    idempotency_key_hash = (
+        f"{function_name}.{module_name}.{function_qualified_name}.{handler_name}#{hash_idempotency_key(data)}"
+    )
     return {
         "ConditionExpression": (
             "attribute_not_exists(#id) OR #expiry < :now OR "
@@ -43,9 +47,13 @@ def build_idempotency_update_item_stub(
     data: Dict,
     handler_response: Dict,
     function_name: str = "test-func",
+    function_qualified_name: str = "test_idempotent_lambda_first_execution_event_mutation.<locals>",
+    module_name: str = "functional.idempotency.test_idempotency",
     handler_name: str = "lambda_handler",
 ) -> Dict:
-    idempotency_key_hash = f"{function_name}.{handler_name}#{hash_idempotency_key(data)}"
+    idempotency_key_hash = (
+        f"{function_name}.{module_name}.{function_qualified_name}.{handler_name}#{hash_idempotency_key(data)}"
+    )
     serialized_lambda_response = json_serialize(handler_response)
     return {
         "ExpressionAttributeNames": {

From 4bbae72cdf7799106295d57a4e93fb0d17095d73 Mon Sep 17 00:00:00 2001
From: Leandro Damascena <leandro.damascena@gmail.com>
Date: Wed, 21 Sep 2022 01:30:47 +0100
Subject: [PATCH 2/9] feat(v2/idempotency): adding end2end tests

---
 tests/e2e/idempotency/__init__.py             |  0
 tests/e2e/idempotency/conftest.py             | 19 +++++++
 .../e2e/idempotency/handlers/basic_handler.py | 11 ++++
 tests/e2e/idempotency/infrastructure.py       | 23 ++++++++
 .../idempotency/test_idempotency_dynamodb.py  | 30 ++++++++++
 tests/e2e/utils/data_fetcher/__init__.py      |  1 +
 tests/e2e/utils/data_fetcher/idempotency.py   | 57 +++++++++++++++++++
 7 files changed, 141 insertions(+)
 create mode 100644 tests/e2e/idempotency/__init__.py
 create mode 100644 tests/e2e/idempotency/conftest.py
 create mode 100644 tests/e2e/idempotency/handlers/basic_handler.py
 create mode 100644 tests/e2e/idempotency/infrastructure.py
 create mode 100644 tests/e2e/idempotency/test_idempotency_dynamodb.py
 create mode 100644 tests/e2e/utils/data_fetcher/idempotency.py

diff --git a/tests/e2e/idempotency/__init__.py b/tests/e2e/idempotency/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/e2e/idempotency/conftest.py b/tests/e2e/idempotency/conftest.py
new file mode 100644
index 00000000000..24a7c71c1f2
--- /dev/null
+++ b/tests/e2e/idempotency/conftest.py
@@ -0,0 +1,19 @@
+import pytest
+
+from tests.e2e.idempotency.infrastructure import IdempotencyDynamoDBStack
+
+
+@pytest.fixture(autouse=True, scope="module")
+def infrastructure(tmp_path_factory, worker_id):
+    """Setup and teardown logic for E2E test infrastructure
+
+    Yields
+    ------
+    Dict[str, str]
+        CloudFormation Outputs from deployed infrastructure
+    """
+    stack = IdempotencyDynamoDBStack()
+    try:
+        yield stack.deploy()
+    finally:
+        stack.delete()
diff --git a/tests/e2e/idempotency/handlers/basic_handler.py b/tests/e2e/idempotency/handlers/basic_handler.py
new file mode 100644
index 00000000000..f4bc6223cd1
--- /dev/null
+++ b/tests/e2e/idempotency/handlers/basic_handler.py
@@ -0,0 +1,11 @@
+from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer, idempotent
+
+persistence_layer = DynamoDBPersistenceLayer(table_name="IdempotencyTable")
+
+
+@idempotent(persistence_store=persistence_layer)
+def lambda_handler(event, context):
+    return {
+        "message": "success",
+        "statusCode": 200,
+    }
diff --git a/tests/e2e/idempotency/infrastructure.py b/tests/e2e/idempotency/infrastructure.py
new file mode 100644
index 00000000000..aad75a20183
--- /dev/null
+++ b/tests/e2e/idempotency/infrastructure.py
@@ -0,0 +1,23 @@
+from aws_cdk import CfnOutput, RemovalPolicy
+from aws_cdk import aws_dynamodb as dynamodb
+
+from tests.e2e.utils.infrastructure import BaseInfrastructure
+
+
+class IdempotencyDynamoDBStack(BaseInfrastructure):
+    def create_resources(self):
+        self.create_lambda_functions()
+        self._create_dynamodb_table()
+
+    def _create_dynamodb_table(self):
+        table = dynamodb.Table(
+            self.stack,
+            "Idempotency",
+            table_name="IdempotencyTable",
+            removal_policy=RemovalPolicy.DESTROY,
+            partition_key=dynamodb.Attribute(name="id", type=dynamodb.AttributeType.STRING),
+            time_to_live_attribute="expiration",
+            billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
+        )
+
+        CfnOutput(self.stack, "DynamoDBTable", value=table.table_name)
diff --git a/tests/e2e/idempotency/test_idempotency_dynamodb.py b/tests/e2e/idempotency/test_idempotency_dynamodb.py
new file mode 100644
index 00000000000..fa6f24fe874
--- /dev/null
+++ b/tests/e2e/idempotency/test_idempotency_dynamodb.py
@@ -0,0 +1,30 @@
+import json
+
+import pytest
+
+from tests.e2e.utils import data_fetcher
+
+
+@pytest.fixture
+def basic_handler_fn(infrastructure: dict) -> str:
+    return infrastructure.get("BasicHandler", "")
+
+
+@pytest.fixture
+def basic_handler_fn_arn(infrastructure: dict) -> str:
+    return infrastructure.get("BasicHandlerArn", "")
+
+
+def test_basic_idempotency_record(basic_handler_fn_arn: str, basic_handler_fn: str):
+    # GIVEN
+    function_name = "basic_handler.lambda_handler"
+    table_name = "IdempotencyTable"
+    payload = json.dumps({"message": "Lambda Powertools"})
+
+    # WHEN
+    data_fetcher.get_lambda_response(lambda_arn=basic_handler_fn_arn, payload=payload)
+
+    # THEN
+    ddb_records = data_fetcher.get_ddb_idempotency_record(function_name=function_name, table_name=table_name)
+
+    assert (ddb_records.get_records()) == 1
diff --git a/tests/e2e/utils/data_fetcher/__init__.py b/tests/e2e/utils/data_fetcher/__init__.py
index be6909537e5..fdd1de5c515 100644
--- a/tests/e2e/utils/data_fetcher/__init__.py
+++ b/tests/e2e/utils/data_fetcher/__init__.py
@@ -1,4 +1,5 @@
 from tests.e2e.utils.data_fetcher.common import get_http_response, get_lambda_response
+from tests.e2e.utils.data_fetcher.idempotency import get_ddb_idempotency_record
 from tests.e2e.utils.data_fetcher.logs import get_logs
 from tests.e2e.utils.data_fetcher.metrics import get_metrics
 from tests.e2e.utils.data_fetcher.traces import get_traces
diff --git a/tests/e2e/utils/data_fetcher/idempotency.py b/tests/e2e/utils/data_fetcher/idempotency.py
new file mode 100644
index 00000000000..bee71071fb1
--- /dev/null
+++ b/tests/e2e/utils/data_fetcher/idempotency.py
@@ -0,0 +1,57 @@
+import boto3
+from retry import retry
+
+
+class DynamoDB:
+    def __init__(
+        self,
+        function_name: str,
+        table_name: str,
+    ):
+        """Fetch and expose Powertools Idempotency key from DynamoDB
+
+        Parameters
+        ----------
+        function_name : str
+            Name of Lambda function to fetch dynamodb record
+        table_name : str
+            Name of DynamoDB table
+        """
+        self.function_name = function_name
+        self.table_name = table_name
+        self.ddb_client = boto3.resource("dynamodb")
+
+    def get_records(self) -> int:
+
+        table = self.ddb_client.Table(self.table_name)
+        ret = table.scan(
+            FilterExpression="contains (id, :functionName)",
+            ExpressionAttributeValues={":functionName": f"{self.function_name}#"},
+        )
+
+        if not ret["Items"]:
+            raise ValueError("Empty response from DynamoDB Repeating...")
+
+        return ret["Count"]
+
+
+@retry(ValueError, delay=2, jitter=1.5, tries=10)
+def get_ddb_idempotency_record(
+    function_name: str,
+    table_name: str,
+) -> DynamoDB:
+    """_summary_
+
+    Parameters
+    ----------
+    function_name : str
+        Name of Lambda function to fetch dynamodb record
+    table_name : str
+            Name of DynamoDB table
+
+    Returns
+    -------
+    DynamoDB
+        DynamoDB instance with dynamodb record
+    """
+    return DynamoDB(function_name=function_name, table_name=table_name)

From 693277532e36eeb0b66eb533498406a02979c20d Mon Sep 17 00:00:00 2001
From: Leandro Damascena <leandro.damascena@gmail.com>
Date: Wed, 21 Sep 2022 10:30:31 +0100
Subject: [PATCH 3/9] feat(v2/idempotency): documentation

---
 docs/utilities/idempotency.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/utilities/idempotency.md b/docs/utilities/idempotency.md
index 7ba61fd3062..f02cd8700b8 100644
--- a/docs/utilities/idempotency.md
+++ b/docs/utilities/idempotency.md
@@ -42,7 +42,7 @@ If you're not [changing the default configuration for the DynamoDB persistence l
 | TTL attribute name | `expiration` | This can only be configured after your table is created if you're using AWS Console |
 
 ???+ tip "Tip: You can share a single state table for all functions"
-    You can reuse the same DynamoDB table to store idempotency state. We add your `function_name` in addition to the idempotency key as a hash key.
+    You can reuse the same DynamoDB table to store idempotency state. We add `module_name` and [qualified name for classes and functions](https://peps.python.org/pep-3155/) in addition to the idempotency key as a hash key.
 
 ```yaml hl_lines="5-13 21-23" title="AWS Serverless Application Model (SAM) example"
 Resources:

From a3ddac7bb0a3be76a0bc8450f5eebf7baaf6c8b5 Mon Sep 17 00:00:00 2001
From: Leandro Damascena <leandro.damascena@gmail.com>
Date: Wed, 21 Sep 2022 16:13:47 +0100
Subject: [PATCH 4/9] feat(v2/idempotency): addressing feedbacks

---
 tests/e2e/idempotency/infrastructure.py       |  9 ++-
 .../idempotency/test_idempotency_dynamodb.py  | 11 +++-
 tests/e2e/utils/data_fetcher/idempotency.py   | 56 +++++++------------
 3 files changed, 33 insertions(+), 43 deletions(-)

diff --git a/tests/e2e/idempotency/infrastructure.py b/tests/e2e/idempotency/infrastructure.py
index aad75a20183..442ac4d4ea4 100644
--- a/tests/e2e/idempotency/infrastructure.py
+++ b/tests/e2e/idempotency/infrastructure.py
@@ -1,15 +1,16 @@
 from aws_cdk import CfnOutput, RemovalPolicy
 from aws_cdk import aws_dynamodb as dynamodb
+from aws_cdk.aws_lambda import Function
 
 from tests.e2e.utils.infrastructure import BaseInfrastructure
 
 
 class IdempotencyDynamoDBStack(BaseInfrastructure):
     def create_resources(self):
-        self.create_lambda_functions()
-        self._create_dynamodb_table()
+        functions = self.create_lambda_functions()
+        self._create_dynamodb_table(function=functions["BasicHandler"])
 
-    def _create_dynamodb_table(self):
+    def _create_dynamodb_table(self, function: Function):
         table = dynamodb.Table(
             self.stack,
             "Idempotency",
@@ -20,4 +21,6 @@ def _create_dynamodb_table(self):
             billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
         )
 
+        table.grant_read_write_data(function)
+
         CfnOutput(self.stack, "DynamoDBTable", value=table.table_name)
diff --git a/tests/e2e/idempotency/test_idempotency_dynamodb.py b/tests/e2e/idempotency/test_idempotency_dynamodb.py
index fa6f24fe874..1599fab6f0e 100644
--- a/tests/e2e/idempotency/test_idempotency_dynamodb.py
+++ b/tests/e2e/idempotency/test_idempotency_dynamodb.py
@@ -15,10 +15,15 @@ def basic_handler_fn_arn(infrastructure: dict) -> str:
     return infrastructure.get("BasicHandlerArn", "")
 
 
-def test_basic_idempotency_record(basic_handler_fn_arn: str, basic_handler_fn: str):
+@pytest.fixture
+def idempotency_table_name(infrastructure: dict) -> str:
+    return infrastructure.get("DynamoDBTable", "")
+
+
+def test_basic_idempotency_record(basic_handler_fn_arn: str, basic_handler_fn: str, idempotency_table_name: str):
     # GIVEN
     function_name = "basic_handler.lambda_handler"
-    table_name = "IdempotencyTable"
+    table_name = idempotency_table_name
     payload = json.dumps({"message": "Lambda Powertools"})
 
     # WHEN
@@ -27,4 +32,4 @@ def test_basic_idempotency_record(basic_handler_fn_arn: str, basic_handler_fn: s
     # THEN
     ddb_records = data_fetcher.get_ddb_idempotency_record(function_name=function_name, table_name=table_name)
 
-    assert (ddb_records.get_records()) == 1
+    assert ddb_records == 1
diff --git a/tests/e2e/utils/data_fetcher/idempotency.py b/tests/e2e/utils/data_fetcher/idempotency.py
index bee71071fb1..109e6735d3b 100644
--- a/tests/e2e/utils/data_fetcher/idempotency.py
+++ b/tests/e2e/utils/data_fetcher/idempotency.py
@@ -2,44 +2,11 @@
 from retry import retry
 
 
-class DynamoDB:
-    def __init__(
-        self,
-        function_name: str,
-        table_name: str,
-    ):
-        """Fetch and expose Powertools Idempotency key from DynamoDB
-
-        Parameters
-        ----------
-        function_name : str
-            Name of Lambda function to fetch dynamodb record
-        table_name : str
-            Name of DynamoDB table
-        """
-        self.function_name = function_name
-        self.table_name = table_name
-        self.ddb_client = boto3.resource("dynamodb")
-
-    def get_records(self) -> int:
-
-        table = self.ddb_client.Table(self.table_name)
-        ret = table.scan(
-            FilterExpression="contains (id, :functionName)",
-            ExpressionAttributeValues={":functionName": f"{self.function_name}#"},
-        )
-
-        if not ret["Items"]:
-            raise ValueError("Empty response from DynamoDB Repeating...")
-
-        return ret["Count"]
-
-
 @retry(ValueError, delay=2, jitter=1.5, tries=10)
 def get_ddb_idempotency_record(
     function_name: str,
     table_name: str,
-) -> DynamoDB:
+) -> int:
     """_summary_
 
     Parameters
@@ -51,7 +18,22 @@ def get_ddb_idempotency_record(
 
     Returns
     -------
-    DynamoDB
-        DynamoDB instance with dynamodb record
+    int
+        Count of records found
+
+    Raises
+    ------
+    ValueError
+        When no record is found within retry window
     """
-    return DynamoDB(function_name=function_name, table_name=table_name)
+    ddb_client = boto3.resource("dynamodb")
+    table = ddb_client.Table(table_name)
+    ret = table.scan(
+        FilterExpression="contains (id, :functionName)",
+        ExpressionAttributeValues={":functionName": f"{function_name}#"},
+    )
+
+    if not ret["Items"]:
+        raise ValueError("Empty response from DynamoDB Repeating...")
+
+    return ret["Count"]

From e6fe6739eaaab78ec13ce297b586a719f44315b0 Mon Sep 17 00:00:00 2001
From: Leandro Damascena <leandro.damascena@gmail.com>
Date: Thu, 22 Sep 2022 12:41:51 +0100
Subject: [PATCH 5/9] feat(v2/idempotency): new e2e tests

---
 .../e2e/idempotency/handlers/basic_handler.py | 11 -------
 .../handlers/ttl_expiration_handler.py        | 14 ++++++++
 tests/e2e/idempotency/infrastructure.py       |  9 ++---
 .../idempotency/test_idempotency_dynamodb.py  | 33 ++++++++++++-------
 4 files changed, 40 insertions(+), 27 deletions(-)
 delete mode 100644 tests/e2e/idempotency/handlers/basic_handler.py
 create mode 100644 tests/e2e/idempotency/handlers/ttl_expiration_handler.py

diff --git a/tests/e2e/idempotency/handlers/basic_handler.py b/tests/e2e/idempotency/handlers/basic_handler.py
deleted file mode 100644
index f4bc6223cd1..00000000000
--- a/tests/e2e/idempotency/handlers/basic_handler.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer, idempotent
-
-persistence_layer = DynamoDBPersistenceLayer(table_name="IdempotencyTable")
-
-
-@idempotent(persistence_store=persistence_layer)
-def lambda_handler(event, context):
-    return {
-        "message": "success",
-        "statusCode": 200,
-    }
diff --git a/tests/e2e/idempotency/handlers/ttl_expiration_handler.py b/tests/e2e/idempotency/handlers/ttl_expiration_handler.py
new file mode 100644
index 00000000000..eabf11e7852
--- /dev/null
+++ b/tests/e2e/idempotency/handlers/ttl_expiration_handler.py
@@ -0,0 +1,14 @@
+import time
+
+from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer, IdempotencyConfig, idempotent
+
+persistence_layer = DynamoDBPersistenceLayer(table_name="IdempotencyTable")
+config = IdempotencyConfig(expires_after_seconds=20)
+
+
+@idempotent(config=config, persistence_store=persistence_layer)
+def lambda_handler(event, context):
+
+    time_now = time.time()
+
+    return {"time": str(time_now)}
diff --git a/tests/e2e/idempotency/infrastructure.py b/tests/e2e/idempotency/infrastructure.py
index 442ac4d4ea4..2a5029517f8 100644
--- a/tests/e2e/idempotency/infrastructure.py
+++ b/tests/e2e/idempotency/infrastructure.py
@@ -1,6 +1,7 @@
+from typing import Any
+
 from aws_cdk import CfnOutput, RemovalPolicy
 from aws_cdk import aws_dynamodb as dynamodb
-from aws_cdk.aws_lambda import Function
 
 from tests.e2e.utils.infrastructure import BaseInfrastructure
 
@@ -8,9 +9,9 @@
 class IdempotencyDynamoDBStack(BaseInfrastructure):
     def create_resources(self):
         functions = self.create_lambda_functions()
-        self._create_dynamodb_table(function=functions["BasicHandler"])
+        self._create_dynamodb_table(function=functions)
 
-    def _create_dynamodb_table(self, function: Function):
+    def _create_dynamodb_table(self, function: Any):
         table = dynamodb.Table(
             self.stack,
             "Idempotency",
@@ -21,6 +22,6 @@ def _create_dynamodb_table(self, function: Function):
             billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
         )
 
-        table.grant_read_write_data(function)
+        table.grant_read_write_data(function["TtlExpirationHandler"])
 
         CfnOutput(self.stack, "DynamoDBTable", value=table.table_name)
diff --git a/tests/e2e/idempotency/test_idempotency_dynamodb.py b/tests/e2e/idempotency/test_idempotency_dynamodb.py
index 1599fab6f0e..2d4bca1d934 100644
--- a/tests/e2e/idempotency/test_idempotency_dynamodb.py
+++ b/tests/e2e/idempotency/test_idempotency_dynamodb.py
@@ -1,4 +1,5 @@
 import json
+from time import sleep
 
 import pytest
 
@@ -6,13 +7,13 @@
 
 
 @pytest.fixture
-def basic_handler_fn(infrastructure: dict) -> str:
-    return infrastructure.get("BasicHandler", "")
+def ttl_expiration_handler_fn(infrastructure: dict) -> str:
+    return infrastructure.get("TtlExpirationHandler", "")
 
 
 @pytest.fixture
-def basic_handler_fn_arn(infrastructure: dict) -> str:
-    return infrastructure.get("BasicHandlerArn", "")
+def ttl_expiration_handler_fn_arn(infrastructure: dict) -> str:
+    return infrastructure.get("TtlExpirationHandlerArn", "")
 
 
 @pytest.fixture
@@ -20,16 +21,24 @@ def idempotency_table_name(infrastructure: dict) -> str:
     return infrastructure.get("DynamoDBTable", "")
 
 
-def test_basic_idempotency_record(basic_handler_fn_arn: str, basic_handler_fn: str, idempotency_table_name: str):
+def test_ttl_expiration_idempotency(ttl_expiration_handler_fn_arn: str, ttl_expiration_handler_fn: str):
     # GIVEN
-    function_name = "basic_handler.lambda_handler"
-    table_name = idempotency_table_name
-    payload = json.dumps({"message": "Lambda Powertools"})
+    payload = json.dumps({"message": "Lambda Powertools - TTL 20 secs"})
 
     # WHEN
-    data_fetcher.get_lambda_response(lambda_arn=basic_handler_fn_arn, payload=payload)
+    # first call
+    first_call, _ = data_fetcher.get_lambda_response(lambda_arn=ttl_expiration_handler_fn_arn, payload=payload)
+    first_call_response = first_call["Payload"].read().decode("utf-8")
 
-    # THEN
-    ddb_records = data_fetcher.get_ddb_idempotency_record(function_name=function_name, table_name=table_name)
+    # second call should return same response as first call
+    second_call, _ = data_fetcher.get_lambda_response(lambda_arn=ttl_expiration_handler_fn_arn, payload=payload)
+    second_call_response = second_call["Payload"].read().decode("utf-8")
+
+    # wait 10s to expire ttl and call again, this should return a new value
+    sleep(20)
+    third_call, _ = data_fetcher.get_lambda_response(lambda_arn=ttl_expiration_handler_fn_arn, payload=payload)
+    third_call_response = third_call["Payload"].read().decode("utf-8")
 
-    assert ddb_records == 1
+    # THEN
+    assert first_call_response == second_call_response
+    assert third_call_response != second_call_response

From f0627992c914cb565cf07e54a9d1f6afcd7e270a Mon Sep 17 00:00:00 2001
From: Leandro Damascena <leandro.damascena@gmail.com>
Date: Thu, 22 Sep 2022 19:52:44 +0100
Subject: [PATCH 6/9] feat(v2/idempotency): adding tests in parallel

---
 .../handlers/parallel_execution_handler.py    | 13 +++
 ...ler.py => ttl_cache_expiration_handler.py} |  0
 .../handlers/ttl_cache_timeout_handler.py     | 15 ++++
 tests/e2e/idempotency/infrastructure.py       |  4 +-
 .../idempotency/test_idempotency_dynamodb.py  | 87 +++++++++++++++----
 tests/e2e/utils/functions.py                  | 11 +++
 6 files changed, 112 insertions(+), 18 deletions(-)
 create mode 100644 tests/e2e/idempotency/handlers/parallel_execution_handler.py
 rename tests/e2e/idempotency/handlers/{ttl_expiration_handler.py => ttl_cache_expiration_handler.py} (100%)
 create mode 100644 tests/e2e/idempotency/handlers/ttl_cache_timeout_handler.py
 create mode 100644 tests/e2e/utils/functions.py

diff --git a/tests/e2e/idempotency/handlers/parallel_execution_handler.py b/tests/e2e/idempotency/handlers/parallel_execution_handler.py
new file mode 100644
index 00000000000..401097d4194
--- /dev/null
+++ b/tests/e2e/idempotency/handlers/parallel_execution_handler.py
@@ -0,0 +1,13 @@
+import time
+
+from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer, idempotent
+
+persistence_layer = DynamoDBPersistenceLayer(table_name="IdempotencyTable")
+
+
+@idempotent(persistence_store=persistence_layer)
+def lambda_handler(event, context):
+
+    time.sleep(10)
+
+    return event
diff --git a/tests/e2e/idempotency/handlers/ttl_expiration_handler.py b/tests/e2e/idempotency/handlers/ttl_cache_expiration_handler.py
similarity index 100%
rename from tests/e2e/idempotency/handlers/ttl_expiration_handler.py
rename to tests/e2e/idempotency/handlers/ttl_cache_expiration_handler.py
diff --git a/tests/e2e/idempotency/handlers/ttl_cache_timeout_handler.py b/tests/e2e/idempotency/handlers/ttl_cache_timeout_handler.py
new file mode 100644
index 00000000000..4de97a4afe4
--- /dev/null
+++ b/tests/e2e/idempotency/handlers/ttl_cache_timeout_handler.py
@@ -0,0 +1,15 @@
+import time
+
+from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer, IdempotencyConfig, idempotent
+
+persistence_layer = DynamoDBPersistenceLayer(table_name="IdempotencyTable")
+config = IdempotencyConfig(expires_after_seconds=1)
+
+
+@idempotent(config=config, persistence_store=persistence_layer)
+def lambda_handler(event, context):
+
+    sleep_time: int = event.get("sleep") or 0
+    time.sleep(sleep_time)
+
+    return event
diff --git a/tests/e2e/idempotency/infrastructure.py b/tests/e2e/idempotency/infrastructure.py
index 2a5029517f8..997cadc4943 100644
--- a/tests/e2e/idempotency/infrastructure.py
+++ b/tests/e2e/idempotency/infrastructure.py
@@ -22,6 +22,8 @@ def _create_dynamodb_table(self, function: Any):
             billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
         )
 
-        table.grant_read_write_data(function["TtlExpirationHandler"])
+        table.grant_read_write_data(function["TtlCacheExpirationHandler"])
+        table.grant_read_write_data(function["TtlCacheTimeoutHandler"])
+        table.grant_read_write_data(function["ParallelExecutionHandler"])
 
         CfnOutput(self.stack, "DynamoDBTable", value=table.table_name)
diff --git a/tests/e2e/idempotency/test_idempotency_dynamodb.py b/tests/e2e/idempotency/test_idempotency_dynamodb.py
index 2d4bca1d934..c5025ca6998 100644
--- a/tests/e2e/idempotency/test_idempotency_dynamodb.py
+++ b/tests/e2e/idempotency/test_idempotency_dynamodb.py
@@ -4,16 +4,22 @@
 import pytest
 
 from tests.e2e.utils import data_fetcher
+from tests.e2e.utils.functions import execute_lambdas_in_parallel
 
 
 @pytest.fixture
-def ttl_expiration_handler_fn(infrastructure: dict) -> str:
-    return infrastructure.get("TtlExpirationHandler", "")
+def ttl_cache_expiration_handler_fn_arn(infrastructure: dict) -> str:
+    return infrastructure.get("TtlCacheExpirationHandlerArn", "")
 
 
 @pytest.fixture
-def ttl_expiration_handler_fn_arn(infrastructure: dict) -> str:
-    return infrastructure.get("TtlExpirationHandlerArn", "")
+def ttl_cache_timeout_handler_fn_arn(infrastructure: dict) -> str:
+    return infrastructure.get("TtlCacheTimeoutHandlerArn", "")
+
+
+@pytest.fixture
+def parallel_execution_handler_fn_arn(infrastructure: dict) -> str:
+    return infrastructure.get("ParallelExecutionHandlerArn", "")
 
 
 @pytest.fixture
@@ -21,24 +27,71 @@ def idempotency_table_name(infrastructure: dict) -> str:
     return infrastructure.get("DynamoDBTable", "")
 
 
-def test_ttl_expiration_idempotency(ttl_expiration_handler_fn_arn: str, ttl_expiration_handler_fn: str):
+def test_ttl_caching_expiration_idempotency(ttl_cache_expiration_handler_fn_arn: str):
     # GIVEN
-    payload = json.dumps({"message": "Lambda Powertools - TTL 20 secs"})
+    payload = json.dumps({"message": "Lambda Powertools - TTL 20s"})
 
     # WHEN
-    # first call
-    first_call, _ = data_fetcher.get_lambda_response(lambda_arn=ttl_expiration_handler_fn_arn, payload=payload)
-    first_call_response = first_call["Payload"].read().decode("utf-8")
+    # first execution
+    first_execution, _ = data_fetcher.get_lambda_response(
+        lambda_arn=ttl_cache_expiration_handler_fn_arn, payload=payload
+    )
+    first_execution_response = first_execution["Payload"].read().decode("utf-8")
 
-    # second call should return same response as first call
-    second_call, _ = data_fetcher.get_lambda_response(lambda_arn=ttl_expiration_handler_fn_arn, payload=payload)
-    second_call_response = second_call["Payload"].read().decode("utf-8")
+    # the second execution should return the same response as the first execution
+    second_execution, _ = data_fetcher.get_lambda_response(
+        lambda_arn=ttl_cache_expiration_handler_fn_arn, payload=payload
+    )
+    second_execution_response = second_execution["Payload"].read().decode("utf-8")
 
-    # wait 10s to expire ttl and call again, this should return a new value
+    # wait 20s to expire ttl and execute again, this should return a new response value
     sleep(20)
-    third_call, _ = data_fetcher.get_lambda_response(lambda_arn=ttl_expiration_handler_fn_arn, payload=payload)
-    third_call_response = third_call["Payload"].read().decode("utf-8")
+    third_execution, _ = data_fetcher.get_lambda_response(
+        lambda_arn=ttl_cache_expiration_handler_fn_arn, payload=payload
+    )
+    third_execution_response = third_execution["Payload"].read().decode("utf-8")
+
+    # THEN
+    assert first_execution_response == second_execution_response
+    assert third_execution_response != second_execution_response
+
+
+def test_ttl_caching_timeout_idempotency(ttl_cache_timeout_handler_fn_arn: str):
+    # GIVEN
+    payload_timeout_execution = json.dumps({"sleep": 10, "message": "Lambda Powertools - TTL 1s"})
+    payload_working_execution = json.dumps({"sleep": 0, "message": "Lambda Powertools - TTL 1s"})
+
+    # WHEN
+    # first call should fail due to timeout
+    execution_with_timeout, _ = data_fetcher.get_lambda_response(
+        lambda_arn=ttl_cache_timeout_handler_fn_arn, payload=payload_timeout_execution
+    )
+    execution_with_timeout_response = execution_with_timeout["Payload"].read().decode("utf-8")
+
+    # the second call should work and return the payload
+    execution_working, _ = data_fetcher.get_lambda_response(
+        lambda_arn=ttl_cache_timeout_handler_fn_arn, payload=payload_working_execution
+    )
+    execution_working_response = execution_working["Payload"].read().decode("utf-8")
+
+    # THEN
+    assert "Task timed out after" in execution_with_timeout_response
+    assert payload_working_execution == execution_working_response
+
+
+def test_parallel_execution_idempotency(parallel_execution_handler_fn_arn: str):
+    # GIVEN
+    arguments = {"lambda_arn": parallel_execution_handler_fn_arn}
+
+    # WHEN
+    # executing Lambdas in parallel
+    execution_result_list = execute_lambdas_in_parallel(
+        [data_fetcher.get_lambda_response, data_fetcher.get_lambda_response], arguments
+    )
+
+    error_idempotency_execution_response = execution_result_list[0][0]["Payload"].read().decode("utf-8")
+    timeout_execution_response = execution_result_list[1][0]["Payload"].read().decode("utf-8")
 
     # THEN
-    assert first_call_response == second_call_response
-    assert third_call_response != second_call_response
+    assert "Execution already in progress with idempotency key" in error_idempotency_execution_response
+    assert "Task timed out after" in timeout_execution_response
diff --git a/tests/e2e/utils/functions.py b/tests/e2e/utils/functions.py
new file mode 100644
index 00000000000..f3fc4c29cc7
--- /dev/null
+++ b/tests/e2e/utils/functions.py
@@ -0,0 +1,11 @@
+from concurrent.futures import ThreadPoolExecutor
+
+
+def execute_lambdas_in_parallel(tasks, arguments):
+    result_list = []
+    with ThreadPoolExecutor() as executor:
+        running_tasks = [executor.submit(task, **arguments) for task in tasks]
+        for running_task in running_tasks:
+            result_list.append(running_task.result())
+
+    return result_list

From b93e4712e6bd8d331da21881d5e53f13bc6f9a32 Mon Sep 17 00:00:00 2001
From: Leandro Damascena <leandro.damascena@gmail.com>
Date: Thu, 22 Sep 2022 23:52:07 +0100
Subject: [PATCH 7/9] feat(v2/idempotency): adding tests in parallel

---
 tests/e2e/idempotency/test_idempotency_dynamodb.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/tests/e2e/idempotency/test_idempotency_dynamodb.py b/tests/e2e/idempotency/test_idempotency_dynamodb.py
index c5025ca6998..46a0723c9fc 100644
--- a/tests/e2e/idempotency/test_idempotency_dynamodb.py
+++ b/tests/e2e/idempotency/test_idempotency_dynamodb.py
@@ -89,8 +89,8 @@ def test_parallel_execution_idempotency(parallel_execution_handler_fn_arn: str):
         [data_fetcher.get_lambda_response, data_fetcher.get_lambda_response], arguments
     )
 
-    error_idempotency_execution_response = execution_result_list[0][0]["Payload"].read().decode("utf-8")
-    timeout_execution_response = execution_result_list[1][0]["Payload"].read().decode("utf-8")
+    timeout_execution_response = execution_result_list[0][0]["Payload"].read().decode("utf-8")
+    error_idempotency_execution_response = execution_result_list[1][0]["Payload"].read().decode("utf-8")
 
     # THEN
     assert "Execution already in progress with idempotency key" in error_idempotency_execution_response

From 4198401f196b1c452c69c5fa13e141c82e2a9a89 Mon Sep 17 00:00:00 2001
From: Leandro Damascena <leandro.damascena@gmail.com>
Date: Sat, 24 Sep 2022 18:17:42 +0100
Subject: [PATCH 8/9] feat(v2/idempotency): refactoring parallel calls to use
 map instead submit

---
 docs/upgrade.md                                    | 6 ++++++
 tests/e2e/idempotency/test_idempotency_dynamodb.py | 7 +++----
 tests/e2e/utils/functions.py                       | 9 ++++++---
 3 files changed, 15 insertions(+), 7 deletions(-)

diff --git a/docs/upgrade.md b/docs/upgrade.md
index 3d1257f1c12..abf04ee2052 100644
--- a/docs/upgrade.md
+++ b/docs/upgrade.md
@@ -142,3 +142,9 @@ You can migrate to the [native batch processing](https://aws.amazon.com/about-aw
 
         return processor.response()
     ```
+
+## Idempotency
+
+We've made a change to the way the Idempotency feature saves the record in DynamoDB. Prior to this change, the record in DynamoDB was saved using the function name and Idempotency key, like this: `lambda_handler#282e83393862a613b612c00283fef4c8`. After this change, the record will be saved using the `module name` + `qualified function name` + `idempotency key`, like this: `app.classExample.function#app.handler#282e83393862a613b612c00283fef4c8`.
+
+You don't need to change anything in your code, but remember that Idempotency records already saved in DynamoDB will have no effect and your executions will generate a new one.
diff --git a/tests/e2e/idempotency/test_idempotency_dynamodb.py b/tests/e2e/idempotency/test_idempotency_dynamodb.py
index 46a0723c9fc..19369b141db 100644
--- a/tests/e2e/idempotency/test_idempotency_dynamodb.py
+++ b/tests/e2e/idempotency/test_idempotency_dynamodb.py
@@ -81,13 +81,12 @@ def test_ttl_caching_timeout_idempotency(ttl_cache_timeout_handler_fn_arn: str):
 
 def test_parallel_execution_idempotency(parallel_execution_handler_fn_arn: str):
     # GIVEN
-    arguments = {"lambda_arn": parallel_execution_handler_fn_arn}
+    arguments = json.dumps({"message": "Lambda Powertools - Parallel execution"})
 
     # WHEN
     # executing Lambdas in parallel
-    execution_result_list = execute_lambdas_in_parallel(
-        [data_fetcher.get_lambda_response, data_fetcher.get_lambda_response], arguments
-    )
+    lambdas_arn = [parallel_execution_handler_fn_arn, parallel_execution_handler_fn_arn]
+    execution_result_list = execute_lambdas_in_parallel("data_fetcher.get_lambda_response", lambdas_arn, arguments)
 
     timeout_execution_response = execution_result_list[0][0]["Payload"].read().decode("utf-8")
     error_idempotency_execution_response = execution_result_list[1][0]["Payload"].read().decode("utf-8")
diff --git a/tests/e2e/utils/functions.py b/tests/e2e/utils/functions.py
index f3fc4c29cc7..7b64c439298 100644
--- a/tests/e2e/utils/functions.py
+++ b/tests/e2e/utils/functions.py
@@ -1,11 +1,14 @@
 from concurrent.futures import ThreadPoolExecutor
 
+from tests.e2e.utils import data_fetcher  # noqa F401
 
-def execute_lambdas_in_parallel(tasks, arguments):
+
+def execute_lambdas_in_parallel(function_name: str, lambdas_arn: list, arguments: str):
     result_list = []
     with ThreadPoolExecutor() as executor:
-        running_tasks = [executor.submit(task, **arguments) for task in tasks]
+        running_tasks = executor.map(lambda exec: eval(function_name)(*exec), [(arn, arguments) for arn in lambdas_arn])
+        executor.shutdown(wait=True)
         for running_task in running_tasks:
-            result_list.append(running_task.result())
+            result_list.append(running_task)
 
     return result_list

From 07d76c8d6c5c77a5d76afb2dd9c212211a28f258 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=BAben=20Fonseca?= <fonseka@gmail.com>
Date: Tue, 27 Sep 2022 14:01:27 +0100
Subject: [PATCH 9/9] chore(docs): added more details to upgrade guide

---
 docs/upgrade.md | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/docs/upgrade.md b/docs/upgrade.md
index abf04ee2052..37e9a318522 100644
--- a/docs/upgrade.md
+++ b/docs/upgrade.md
@@ -12,6 +12,7 @@ Changes at a glance:
 
 * The API for **event handler's `Response`** has minor changes to support multi value headers and cookies.
 * The **legacy SQS batch processor** was removed.
+* The **Idempotency key** format changed slightly, invalidating all the existing cached results.
 
 ???+ important
     Powertools for Python v2 drops suport for Python 3.6, following the Python 3.6 End-Of-Life (EOL) reached on December 23, 2021.
@@ -143,8 +144,13 @@ You can migrate to the [native batch processing](https://aws.amazon.com/about-aw
         return processor.response()
     ```
 
-## Idempotency
+## Idempotency key format
 
-We've made a change to the way the Idempotency feature saves the record in DynamoDB. Prior to this change, the record in DynamoDB was saved using the function name and Idempotency key, like this: `lambda_handler#282e83393862a613b612c00283fef4c8`. After this change, the record will be saved using the `module name` + `qualified function name` + `idempotency key`, like this: `app.classExample.function#app.handler#282e83393862a613b612c00283fef4c8`.
+The format of the Idempotency key was changed. This is used store the invocation results on a persistent store like DynamoDB.
 
-You don't need to change anything in your code, but remember that Idempotency records already saved in DynamoDB will have no effect and your executions will generate a new one.
+No changes are necessary in your code, but remember that existing Idempotency records will be ignored when you upgrade, as new executions generate keys with the new format.
+
+Prior to this change, the Idempotency key was generated using only the caller function name (e.g: `lambda_handler#282e83393862a613b612c00283fef4c8`).
+After this change, the key is generated using the `module name` + `qualified function name` + `idempotency key` (e.g: `app.classExample.function#app.handler#282e83393862a613b612c00283fef4c8`).
+
+Using qualified names prevents distinct functions with the same name to contend for the same Idempotency key.