OneApp is a customer-facing platform built by E.ON Germany to bring commodity energy, solar, e-mobility, flexibility, and partner services into one experience.
A customer can open the E.ON app and ask, for example, how purchasing and installing a heat pump would impact their energy expenses. The home screen rearranges itself to answer: a personalized explanation of the functionality of heat pumps and products available through E.ON, an estimated comparison against the same months last year if the customer would have had a heat pump, and a tariff option better suited to the new setup.
Nobody designed that screen. It was assembled on the spot from components owned by three different teams, using data held in three systems with three different identity models.
The generative AI features of the app work because of two key mechanisms: a versioned composition contract that lets a screen be assembled from independently deployed micro-frontends (MFEs), and an identity trust broker that turns one login into the credential each backend already trusts. A core idea of the solution is that it doesn’t remove complexity, it moves complexity somewhere it can be named, versioned, and tested.
This post discusses how E.ON Germany built the solution using Amazon Web Services (AWS), and shows how the AI layer reuses these key components.
Challenges of a fragmented architecture
A household with a heat pump, a wallbox, solar panels, a battery, and a dynamic tariff deals with a separate app for each, by different companies. Each has its own login, its own data freshness, and its own view of what a kilowatt-hour is worth. Nobody designed that landscape either. It accreted across the industry.
The problem is structural, not cosmetic, and it costs the customer money rather than patience. Value in energy no longer sits inside any single product. It sits in the coordination between them. E.ON Germany maps that value into five flexibility pools: consumption cost optimization, regulatory incentives such as time-variable grid fees, advanced market participation, self-generation, and the commodity contract itself. Coordinating across all five pools has the potential to reduce a household’s energy cost substantially, though the combined effect has not been independently sized at scale. Published studies size the pieces individually: around €600 per year (long-term, 2035 projection) from shifting consumption on a dynamic tariff, photovoltaic (PV) self-consumption rising from roughly 40–60 percent to 70–85 percent with a battery sized at 3 kWh per kWp of PV system (2014 study), reduced and time-variable grid fees for controllable devices under §14a EnWG, and up to €900–1,200 per year when one home energy management system coordinates solar panels, heat pump, and an electric vehicle (EV) together (2030 modeling scenario).
The last finding is the architectural argument. The savings compound only under joint optimization: a battery charged for self-consumption at midday isn’t available for price arbitrage in the afternoon, and a heat pump pre-heating into a grid-fee window competes with EV charging for the same house connection. Joint optimization needs one control point with a live view of tariffs, assets, and customer preference, and that control point can’t exist while each product owns a slice of the state.
The fragmentation that stops the app from explaining a bill is the same fragmentation that caps what the customer can earn from their own flexibility.
The following diagram compares a fragmented solution to E.ON’s OneApp.
Solution: Build a shell, not an app
Extending the existing commodity app was the obvious alternative. E.ON rejected it, because the binding constraint was never screen real estate, it was release coupling. Commodity, solar, e-mobility, flexibility, and partner services differ in teams, regulatory surface, release cadence, and in several cases company.
Therefore, the shell owns only what has to be consistent for every customer: identity and authentication context, navigation, feature flags and the cross-domain event bus, the design system, and client-side observability. Everything else is an independently deployed MFE, and each domain owns its bundle, its backend-for-frontend (BFF), its contracts, and its pipeline.
Every domain integrates on identical terms. E.ON’s own products (commodity, solar, car, and E.ON Home, its home energy platform) sit alongside partner products such as gridX as peers behind one contract, not as tiers stacked on each other. That symmetry is deliberate: onboarding a partner becomes the same operation as shipping an internal domain, which keeps a partner strategy from turning into an architecture project every time. The shell runs on the web using Ionic Capacitor and as native iOS and Android apps.
A shell is a trade-off, not a free win. E.ON accepted two compromises on purpose, and in both cases decided up front where the resulting complexity would sit:
- Integrate one flexibility provider properly rather than build a provider-agnostic abstraction on day one – The coupling this integration creates is real, but it lives in a single place: the adapter behind that domain’s BFF. Onboarding a second provider means writing another adapter, not reworking the platform.
- Allow exceptions to serverless-first – A few workloads hold long-lived state, so they run on containers rather than functions. Everything around them stays constant (the API facade, the authentication path, the event bus), so a different runtime doesn’t turn into a different architecture.
The manifest is a control plane, not a config file
The shell doesn’t know at build time which MFE to render for a domain. Composition uses Webpack Module Federation: each domain publishes a remote container (remoteEntry.js) that the shell loads at runtime. The manifest maps a domain, a shell contract version, and a user segment to a specific bundle version and location. The following code shows an example (not E.ON’s literal schema):
That single indirection turns frontend composition into an operational control plane. Four properties follow.
Multiple live versions, not one current version
The manifest holds several valid combinations at the same time, so the solar team can route a canary cohort to a new major version while everyone else stays on the previous one. This needs no shell rebuild and no coordination with other domains.
Today, rollback maturity varies across domains. Some already roll back by flipping a manifest entry: when golden signals for a bundle version degrade (render latency, client error rate, real-user monitoring), on-call updates the manifest and the fleet drains back to the last good version within one manifest cache lifetime. No rebuild, no redeploy. Rolling back the other domains still requires an update submitted through the app stores. The app store policies on remotely loaded interpreted code decide which changes we can make server-side without shipping a new binary. We’re moving those remaining domains to server-side configuration one at a time, and the goal is that every domain can be rolled back through configuration alone.
Manifest cache lifetime is one of several blast radius controls, but it’s the one the team sizes most deliberately. It sets the upper bound on rollout and rollback propagation at the content delivery network (CDN) edge: too long and a bad canary outlives the incident call, too short and every cold start pays for a fetch. It doesn’t, however, govern all layers. Browser caches, service-worker caches, and stale-while-revalidate behavior can each extend how long a stale manifest survives beyond a CDN invalidation. The Time to Live (TTL) belongs in the runbook as an explicit choice, alongside invalidation and client-refresh policies, not inherited from a CDN default.
The contract, not the bundle, is the compatibility unit
Each MFE declares which shell contract versions it supports, and each MFE’s continuous integration (CI) pipeline self-attests compatibility by running its own tests against the current and previous shell contract before any traffic is routed. This is provider-side verification: it confirms that the MFE works against the shell versions it declares, but it can’t catch undeclared dependencies in the other direction. The shell’s own integration suite covers that gap at a coarser cadence. That N-1 rule lets the shell and the domains deploy on unrelated schedules. Breaking changes get a new contract major and a deprecation window counted in shell releases.
Two failure modes are worth designing for, because neither is visible in a single pull request:
- Bundle-size drift – Independent teams each add one dependency, no individual change looks bad, and time-to-interactive degrades collectively, so the performance budget belongs in CI per MFE, as a failing build.
- Event-bus coupling – The bus is where two domains quietly become dependent on each other, so bus events need owners, schemas, and versions exactly like the HTTP contracts.
Because the manifest decides which bundles load, it’s the highest-value target in the supply chain. A sha384 on the bundle entry is meaningless if an attacker can swap the manifest and the hash together. Three controls protect it: the manifest is served exclusively through a protected origin (Amazon CloudFront with origin access control), so no public write path exists. Publishing is restricted to an authorized CI pipeline whose deploy credentials are scoped, rotated, and audit-logged. And every published version is immutable and content-addressed, so a rollback always points to a known-good artifact rather than a mutable path. MFE remotes execute in the shell’s browser context, so each partner remote is treated as trusted code. Component-level isolation isn’t enforced by integrity checks or Content Security Policy (CSP) alone. It relies on the aforementioned publishing controls and on code review before a remote is admitted to the registry.
The integrity field in the manifest carries a Subresource Integrity (SRI) hash for the remote entry point. Module Federation also loads asynchronous chunks on demand. The custom loader extends SRI verification to those chunks by injecting integrity metadata at build time. This protects against CDN-level tampering of individual files, but it isn’t a substitute for origin-level controls: if the manifest itself is compromised, the hashes are compromised with it. A CSP with a strict script-src directive provides the complementary browser-side control, restricting executable sources to the known CDN origins.
The following diagram illustrates how the shell resolves MFE bundles using CloudFront.
One login, several trust worlds
“One login, one E.ON” is cheap to say and expensive to build, because the domains behind it don’t share an identity model: some backends are backed by customer relationship management (CRM) systems, others by Amazon Cognito, and partner domains arrive with their own trust models. Migrating everything onto one new identity provider would have been a multi-year program that blocked the product meanwhile. E.ON took the other path: centralize authentication, decentralize authorization.
The customer authenticates once against the central customer identity and access management (CIAM) system. The shell BFF then acts as a trust broker, exchanging that identity for the credential each downstream backend already trusts. Following the solar request from the opening example, the workflow consists of the following steps:
- The app presents its CIAM token, obtained through an OAuth 2.0 authorization code flow with Proof Key for Code Exchange (PKCE). The BFF validates signature, issuer, audience, and expiry against a cached key set, with key rotation and revocation handled in line with OpenID Connect (OIDC) best practices.
- The BFF resolves the domain’s trust profile. Solar is Amazon Cognito backed, so the BFF validates the inbound CIAM access token and obtains a domain-scoped credential on behalf of the user. Amazon Cognito user pools don’t support RFC 8693 token exchange natively. Instead, an AWS Lambda authorizer validates the CIAM token’s signature and claims, then issues a session-bound credential scoped to the solar resource server’s custom scopes. The domain authorizes on those scope claims and doesn’t need to trust CIAM directly. The credential is held in server-side session state, so it doesn’t reach the client.
- The exchanged token is cached against the session, below its own expiry, so a screen refresh doesn’t trigger another exchange. The same code path mints a CRM-shaped credential for a CRM-backed domain. One screen, three trust models, one customer login.
The design questions that matter are the ones that only surface at scale:
- Blast radius – The broker sits on the critical path for every domain call, so its behavior under failure is an architecture decision rather than an operational detail: which domains fail closed, which serve cached data, and for how long.
- Revocation lag – A cached exchanged token outlives a logout or a central revocation until it expires, so the cache needs an explicit invalidation path. In practice, this is a bus event that purges broker state on logout, password reset, or contract change.
- Audience scoping – Exchanged tokens must be audience-restricted per domain. A broker that mints one broadly scoped token for convenience recreates the shared-secret problem it was built to remove.
- Auditability – Every exchange must be traceable back to the originating CIAM subject and request ID, or cross-domain incident forensics becomes guesswork.
For E.ON, the goal was never to consolidate identity providers, but to build a trust broker that speaks all of their languages.
The following diagram illustrates the authentication workflow.
Behind a domain: The same idea, one layer down
Containment isn’t only a frontend pattern. E.ON Home, the home energy domain, runs its own platform behind its BFF on the same principle: put the awkward parts in one identifiable place.
Telemetry from connected assets and original equipment manufacturer (OEM) systems ingests through Amazon Simple Queue Service (Amazon SQS), which buffers downstream work so a slow consumer doesn’t throttle producers. Consumers are idempotent on the natural key of asset, measurement type, and timestamp, because at-least-once delivery plus device retries make duplicate readings normal rather than exceptional. Idempotency turns rereading a stream during an incident into a routine operation instead of a data-integrity event. Amazon ElastiCache holds transient state in memory, avoiding a round trip to Amazon Aurora for read-heavy asset queries. Stateless work runs on Lambda. Workloads holding long-lived state run on Amazon Elastic Container Service (Amazon ECS), because Lambda caps execution at 900 seconds and doesn’t guarantee state persistence between invocations. Persistence spans Aurora, Amazon DynamoDB, and Amazon Simple Storage Service (Amazon S3), and Amazon EventBridge distributes flexibility and bonus events to domains with matching rules. Delivery is at-least-once with no ordering guarantee, so consumers must be idempotent and order-tolerant.
The following diagram illustrates this architecture.
Results: An AI feature that assembles a screen
Returning to the opening question of how purchasing and installing a heat pump would impact energy expenses, answering it meant reading contract data, consumption history, customers’ assets, weather historical data, an E.ON knowledge base, and tariff options that live in different domains. From those inputs, the system produced a screen that exists in no build artifact, displaying content relevant to the customer’s question and sorted by topic. The result is a personalized, contextual narrative, not a generic answer.
Amazon Cognito gates access to user and contract data. Lambda orchestrates retrieval and prompt construction. Documents are chunked by semantic boundary (one chunk per invoice line item, contract clause, or tariff component) and embedded using Amazon Titan Embeddings, with the index refreshed on each billing cycle. Amazon OpenSearch Service supplies context through knn_vector fields, approximate k-NN, and hybrid queries that combine lexical (BM25) and vector (k-NN scoring) through a normalization processor, which matters because invoice terminology and cost-position names must be retrievable by exact terms, whereas the customer’s phrasing doesn’t. The pipeline is hand-built rather than managed through Amazon Bedrock Knowledge Bases because E.ON needed control over hybrid scoring weights, domain-specific chunking logic, and the ability to blend structured contract data with unstructured document retrieval in a single query. Amazon Bedrock parses intent, applies guardrails, and generates the explanation.
The model never returns markup or code, which is what makes this shippable. It returns a UI assembly plan: which registered components to render, in what order, bound to which retrieved figures, validated against an allowlisted component registry before the shell renders anything. Bind values are schema-validated per component and treated strictly as data. The shell never evaluates them as markup or executable code, so an allowlisted component can’t become a delivery mechanism for injected content. The following code provides an example:
That plan is the same contract the manifest routes. The AI layer didn’t need a new composition mechanism. It needed the one that already existed, which is why the feature could ship as a manifest entry and a canary cohort rather than as an app release.
Two guardrail policies enforce safety, and both are configured using Amazon Bedrock Guardrails rather than instructions in a prompt:
- Sensitive information filters detect personally identifiable information (PII) and either mask it, replacing values with type tags, or block the content entirely and return a canned response, using built-in entity types plus custom regular expressions. This method is necessary here because meter, contract, and market location identifiers (MaLo/MeLo) aren’t standard PII types and must be declared explicitly.
- A contextual grounding check evaluates the response against the supplied grounding source and query, so an explanation whose grounding scores fall below threshold is blocked rather than passed through.
This is one layer of defense when the output discusses money the customer owes, though it doesn’t catch every failure mode. A real figure placed in the wrong context, for instance, requires structured output validation upstream. Sensitive information filters can also run in detect mode, taking no action but reporting what they would have flagged, which is how a policy change gets measured against real traffic before it’s enforced. Contextual grounding operates on threshold scores rather than an action selector, so it has no equivalent detect mode.
A second feature, Invoice Explanation with AI, runs the same pipeline against the same contract: a customer asks why costs rose year-over-year despite similar consumption, and the model chooses and prioritizes components rather than composing prose.
The following diagram illustrates the solution workflow.
Figure 5: AI-assisted explanation flow using Amazon API Gateway, AWS Lambda, Amazon OpenSearch Service, and Amazon Bedrock
Conclusion
OneApp scales delivery through a versioned contract: autonomous domains ship independently while the customer still sees one E.ON. It scales understanding through a grounded AI layer that reuses that same contract to assemble answers instead of screens. Both are the same design idea applied twice. Put the complexity behind a boundary that has a name, a version, and a test.
For E.ON, the platform is never finished. E.ON uses the term New Energy market for solar, heat pumps, storage, flexibility, and related services beyond commodity supply. As that market changes, the architecture changes with it. The test of the design is that the next energy product, or the next partner, arrives as an adapter and a manifest entry, and the customer never sees the seam.
To explore the AWS services discussed in this post, see Detect and filter harmful content by using Amazon Bedrock Guardrails, Scopes, M2M, and resource servers, and Amazon OpenSearch Service hybrid search. If you’re building a similar multi-domain platform and would like to discuss architecture patterns, contact the authors or your AWS account team.





