An inventory scheme that establishes a source of truth would be to query these groups from some system. Something more sophisticated than strings in the DNS name.
For example, netbox inventory database system has a device role that could fit.  Not to pick on netbox, but it has an Ansible inventory plugin, and an open database model so is easy to talk about.
However, maybe a nice external database does not exist yet. Such a regular naming scheme can be generated.  With that most recursive of Ansible inventory plugins, generator:
# inventory.yml
plugin: generator
hosts:
    name: "{{ application }}{{ number }}.{{ environment }}.example.com"
    parents:
      - name: "{{ application }}_{{ environment }}"
        parents:
          - name: "{{ application }}"
            vars:
              application: "{{ application }}"
          - name: "{{ environment }}"
            vars:
              environment: "{{ environment }}"
layers:
    application:
        - app
        - api
    environment:
        - dev
        - qa
        - uat
        - prod
    number:
        - "01"
        - "02"
        - "05"
Layer names are arbitrary. Given the "hosts" root and the "parents" notation, deeper indented names are groups that contain the outer names.
ansible-inventory  -i inventory.yml --list  will print hosts in Ansible's inventory JSON document.  Partial output:
{
    "_meta": {
        "hostvars": {
           "app05.qa.example.com": {
                "application": "app",
                "environment": "qa"
            }
        }
    },
    "app_qa": {
        "hosts": [
            "app01.qa.example.com",
            "app02.qa.example.com",
            "app05.qa.example.com"
        ]
    },
    "app": {
        "children": [
            "app_dev",
            "app_prod",
            "app_qa",
            "app_uat"
        ]
    },
    "qa": {
        "children": [
            "api_qa",
            "app_qa"
        ]
    },
}
And continuing on for other combinations.
Notice it made:
- "application" groups
- "environment" groups
- "application environment" combination groups
- host names conforming to the DNS names pattern
- vars containing the "application" and "environment" for each host
Limitations of this plugin include:
Always doing the cartesian product of the layer combinations. Cannot have more or less of a certain group, nor start the numbering schemes on different values.
Not having compact host ranges. Neither [01:25] syntax nor the range() function works. Consider requesting that by filing an issue. As a workaround, dozens of numbers in the config file is functional.