Scaling Flutter development: UI composition in a multi-team setup

Scaling Flutter development: UI composition in a multi-team setup

As Funda’s mobile app and engineering organisation grew, so did the challenge of enabling multiple teams to contribute independently without creating unnecessary complexity. Frontend Developer Felipe Porfirio Fuzaro explains how UI composition principles helped us scale Flutter development while maintaining clear ownership and a consistent app experience.

As organizations grow, so do their products and the number of teams working on them. When multiple teams contribute to the same application, coordination quickly becomes one of the biggest challenges. Without clear boundaries, even small changes can require extensive alignment. Shared screens can turn into bottlenecks, UI components may be changed for one use case while unintentionally affecting another, and teams end up spending more time aligning than delivering. These were some challenges we faced at Funda and we needed a way to let teams move faster and more independently, without losing coherence in the app or creating a complex, hard-to-manage codebase.

From centralized app teams to end-to-end ownership

For a long time, mobile development at Funda lived in a dedicated apps team, responsible for native Android and iOS. As the company evolved, we moved towards end-to-end teams: cross-functional teams that own a domain from idea to delivery, including UI, business logic and long-term maintenance. Around the same time, we adopted Flutter for our mobile app.

See also: Leaving Native behind: Our transition to an integrated Flutter experience

This combination opened up an opportunity. If other disciplines were already working with microservices and clear ownership boundaries, could we apply similar principles to the mobile app?

Feature teams and autonomy

We work with feature teams, where each team owns specific features of the app and is responsible for them end-to-end. Some examples of feature teams that support the app are:

  • Insights: responsible for features that provide users with valuable insights about their home and the market.
  • Connect: working on features that connect consumers to agents.
  • Partnerships: helping partners reach their audience through Funda.

Each team owns its roadmap, priorities and delivery within its domain. To support this level of autonomy, we needed an architecture that scaled across teams without turning the app into a shared monolith. Our answer was to structure the Flutter app around UI composition, inspired by micro frontend principles.

Packages as building blocks

At the core of this approach is the idea that packages are the unit of ownership. Feature teams own one or more packages that encapsulate the feature's UI and domain logic.

These packages expose only what is needed to integrate with the rest of the app:

  • Widgets
  • Services
  • Shared models
Figure 1: teams' ownership in a listing details screen

The feature code itself stays internal to the package and is owned by the team responsible for that domain.

The main application acts as an app shell. Its responsibility is to wire these feature packages together and compose the final UI into a single Flutter app. It provides shared infrastructure by acting as the composition root, but it deliberately avoids implementing feature-specific logic. This keeps the app shell thin and reinforces clear ownership boundaries.

Keeping teams independent

To keep teams independent, we are strict about what is shared across package boundaries. Instead of depending on each other’s internal code, teams interact through small, explicit contracts. Common building blocks exist, but they are treated as stable foundations rather than places to implement feature logic. This makes dependencies explicit and helps prevent tight coupling as the codebase grows.

See also: Game changer: the benefits of having a lighthouse architecture

Cross-cutting concerns and platform ownership

Cross-cutting concerns are handled separately from feature development. At Funda, these are coordinated and implemented by a dedicated team called Platform Apps. This team is responsible for concerns that cut across all features, such as:

  • Navigation
  • Authentication
  • Push notifications
  • Localization

Rather than letting these concerns spread into feature packages in ad-hoc ways, they are provided through well-defined abstractions.

Technically, these cross-cutting concerns are shared using the dependency inversion principle. Platform, or core packages as we call them, provide concrete implementations that conform to shared interfaces. These implementations are created and injected at the root composition level in the app shell. Feature packages depend only on the abstractions, not on the concrete implementations, and receive access to the dependencies through this composition. This keeps feature teams decoupled from the underlying infrastructure while still using common capabilities in a consistent way.

Figure 2: inversion of dependencies in the app

It also significantly improves testability: because features depend only on interfaces, tests can replace core services with mocks or fakes that control behaviour and edge cases. This enables fast, isolated unit tests without requiring real infrastructure, making tests easier to write, more reliable and more focused on feature logic.

Micro frontends, adapted to Flutter

This setup is inspired by micro frontends, but it is not a direct translation of the web model. On the web, micro frontends often rely on runtime composition, where independently deployed UI modules are loaded dynamically. In Flutter, we always ship a single compiled application bundle, so independent deployment of feature UIs is not possible.

Instead, we focus on build-time composition. Teams develop their feature packages independently, behind clear boundaries and contracts. At build time, the app shell composes everything into a single release. The result is not independent deployment, but independent development, which is what we need to scale a multi-team Flutter codebase.

What it has brought us

In practice, this structure has worked well:

  • Ownership is clearer because feature code lives with the teams that own the domain.
  • Teams can iterate faster because most changes stay within their packages.
  • Cross-team friction is reduced by limiting shared code to well-defined contracts and stable foundations.

There are still trade-offs. We ship a single Flutter app, which means a single release train. Cross-cutting changes still require coordination.

The architecture also relies heavily on discipline. Boundaries are a convention, not a hard technical constraint. Keeping the system healthy requires teams to consistently respect dependency rules, maintain stable public APIs, and avoid moving feature-specific logic into shared or platform layers for short-term convenience. When shared layers stay focused on infrastructure and abstractions, the overall structure remains scalable and easier to reason over time.

Do you have a burning question for Felipe after reading his blog? Feel free to reach out to him via email.

We're hiring! We're looking for an amazing Flutter Engineer. Check out our vacancies here.

Great! Next, complete checkout for full access to Funda Engineering blog.
Welcome back! You've successfully signed in.
You've successfully subscribed to Funda Engineering blog.
Success! Your account is fully activated, you now have access to all content.
Success! Your billing info has been updated.
Your billing was not updated.