flutter
/

GetX Feature Modular Architecture: Scalable Project Structure

Last Sync: Today

On this page

13
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

GetX Feature Modular Architecture: Scalable Project Structure

Introduction

As your Flutter app grows, keeping all your code in a single flat folder becomes unsustainable. A feature‑modular architecture organizes your app by features (e.g., authentication, products, cart) rather than by technical layer (controllers, views). GetX’s dependency injection and routing system are perfectly suited for this approach. This guide shows how to structure your app into independent, lazy‑loaded modules that are easy to maintain, test, and scale.

Real-World Use Cases

  • E‑commerce App – Separate modules for auth, products, cart, checkout, orders – each can be developed in parallel.
  • Social Media App – Modules for feed, profile, messaging, notifications – lazy loading reduces startup time.
  • Enterprise Apps – Feature teams own their modules, reducing merge conflicts and enabling independent testing.

How It Works: Module Flow

When the app starts, only the initial binding and shared services are loaded. When a user navigates to a feature (e.g., /products), GetX looks up the GetPage definition, calls the binding’s dependencies() method, and registers the module’s controllers and services lazily. The page is then built. This ensures that modules are only loaded when needed, keeping the app fast.

What is a Feature Module?

A feature module is a self‑contained unit that encapsulates all the code related to a specific feature: UI (pages), business logic (controllers), data (models/repositories), and routing (bindings). Modules are independent – they can be developed, tested, and even reused across projects. GetX enables this through GetPage and bindings that load only the required modules when the route is accessed.

Recommended Folder Structure

TEXTRead-only
1
lib/
├── main.dart
├── app/
│   ├── routes/
│   │   ├── app_pages.dart
│   │   └── app_routes.dart
│   ├── shared/
│   │   ├── widgets/
│   │   ├── utils/
│   │   └── services/
│   └── modules/
│       ├── auth/
│       │   ├── bindings/
│       │   │   └── auth_binding.dart
│       │   ├── controllers/
│       │   │   └── auth_controller.dart
│       │   ├── views/
│       │   │   ├── login_page.dart
│       │   │   └── register_page.dart
│       │   └── models/
│       │       └── user_model.dart
│       ├── home/
│       │   ├── bindings/
│       │   │   └── home_binding.dart
│       │   ├── controllers/
│       │   │   └── home_controller.dart
│       │   └── views/
│       │       └── home_page.dart
│       └── product/
│           ├── bindings/
│           │   └── product_binding.dart
│           ├── controllers/
│           │   └── product_controller.dart
│           ├── views/
│           │   ├── product_list_page.dart
│           │   └── product_detail_page.dart
│           ├── models/
│           │   └── product.dart
│           └── repositories/
│               └── product_repository.dart

Creating a Module

Each module typically consists of:

  • bindings/ – Registers the module’s controllers and dependencies.
  • controllers/ – Business logic, state, and workers.
  • views/ – UI pages that use the controllers.
  • models/ – Data classes (optional).
  • repositories/ – API or data access logic (optional).
DARTRead-only
1
// product/bindings/product_binding.dart
import 'package:get/get.dart';

class ProductBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut<ProductRepository>(() => ProductRepository());
    Get.lazyPut<ProductController>(() => ProductController(Get.find()));
  }
}

// product/controllers/product_controller.dart
class ProductController extends GetxController {
  final ProductRepository repository;
  ProductController(this.repository);

  var products = <Product>[].obs;
  var isLoading = false.obs;

  @override
  void onInit() {
    super.onInit();
    fetchProducts();
  }

  Future<void> fetchProducts() async {
    isLoading.value = true;
    try {
      products.value = await repository.getProducts();
    } finally {
      isLoading.value = false;
    }
  }
}

Routing with Modules

Define all routes in app/routes/app_routes.dart and app_pages.dart. Each route can specify its own binding, which loads only the required module when the route is visited. This enables lazy loading and keeps the initial app small.

DARTRead-only
1
// app/routes/app_routes.dart
part of 'app_pages.dart';

class AppRoutes {
  static const String login = '/login';
  static const String home = '/home';
  static const String products = '/products';
  static const String productDetail = '/product/:id';
}

// app/routes/app_pages.dart
import 'package:get/get.dart';
import '../../modules/auth/bindings/auth_binding.dart';
import '../../modules/auth/views/login_page.dart';
import '../../modules/home/bindings/home_binding.dart';
import '../../modules/home/views/home_page.dart';
import '../../modules/product/bindings/product_binding.dart';
import '../../modules/product/views/product_list_page.dart';
import '../../modules/product/views/product_detail_page.dart';

part 'app_routes.dart';

class AppPages {
  static const INITIAL = AppRoutes.login;

  static final routes = [
    GetPage(
      name: AppRoutes.login,
      page: () => LoginPage(),
      binding: AuthBinding(),
    ),
    GetPage(
      name: AppRoutes.home,
      page: () => HomePage(),
      binding: HomeBinding(),
    ),
    GetPage(
      name: AppRoutes.products,
      page: () => ProductListPage(),
      binding: ProductBinding(),
    ),
    GetPage(
      name: AppRoutes.productDetail,
      page: () => ProductDetailPage(),
      binding: ProductBinding(), // can reuse binding
    ),
  ];
}

Communication Between Modules

Modules can communicate via shared services (like AuthService placed in app/shared/services). Since the service is registered globally (e.g., in initialBinding), any module can access it using Get.find<AuthService>(). This keeps modules loosely coupled.

DARTRead-only
1
// app/shared/services/auth_service.dart
class AuthService extends GetxService {
  var isLoggedIn = false.obs;

  void login() => isLoggedIn.value = true;
  void logout() => isLoggedIn.value = false;
}

// In main.dart or initialBinding
void main() async {
  await GetStorage.init();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      initialBinding: AppBinding(), // registers AuthService
      getPages: AppPages.routes,
      initialRoute: AppPages.INITIAL,
    );
  }
}

class AppBinding extends Bindings {
  @override
  void dependencies() {
    Get.put(AuthService(), permanent: true);
  }
}

Comparison: Modular vs Flat Architecture

AspectModular ArchitectureFlat Architecture
ScalabilityHigh – easy to add new featuresLow – becomes messy quickly
Team CollaborationTeams can own separate modulesHigh risk of merge conflicts
Startup PerformanceExcellent – lazy load modulesAll code loaded upfront
TestabilityHigh – modules testable in isolationMedium – dependencies scattered
Code ReuseEasy to extract modulesDifficult
Learning CurveSteeper initiallyGentle

Lazy Loading & Performance

Because each module is registered via Get.lazyPut inside its binding, the controllers and services are only created when the route is visited. This significantly reduces startup time and memory usage. You can also set permanent: true for global services that must stay alive.

Best Practices

  • Keep modules self‑contained – Each module should ideally not import other modules directly (except via shared services).
  • Use shared services for cross‑module communication – This keeps the dependency graph clean.
  • Lazy load modules – Use Get.lazyPut in bindings and avoid global registrations unless necessary.
  • Use constants for route names – Prevents typos and makes refactoring easier.
  • Place each module in its own folder – Makes it easy to move or reuse modules later.
  • Keep shared code in app/shared – Reusable widgets, utilities, and services.
  • Test modules in isolation – Because modules are independent, you can test them separately.
  • Use GetPage with bindings – Never register controllers outside of bindings for modules.

Common Mistakes

  • ❌ Creating circular dependencies between modules – Module A imports Module B, Module B imports Module A. ✅ Use shared services to break cycles.
  • ❌ Putting all routes in one giant GetPage list without module separation – Still works, but not modular. ✅ Define routes with their respective bindings.
  • ❌ Using Get.put inside views instead of bindings – This scatters the dependency registration. ✅ Keep registration inside bindings.
  • ❌ Not providing a binding for a route – The controllers may not be registered. ✅ Always define a binding for each route.
  • ❌ Placing module code outside the modules/ folder – Makes it hard to locate features. ✅ Keep modules under lib/app/modules/.

Conclusion

A feature‑modular architecture using GetX gives you a scalable, maintainable app structure. By separating concerns into independent modules, you enable better team collaboration, easier testing, and the ability to load only what’s needed. Combined with GetX’s lazy loading and dependency injection, it’s a powerful pattern for any Flutter project.

Test Your Knowledge

Q1
of 3

Where should you place the binding for a module?

A
Inside the view file
B
In the module’s bindings folder, registered with GetPage
C
In main.dart
D
In a shared services file
Q2
of 3

How do modules communicate without creating direct dependencies?

A
Using global variables
B
Through shared services registered in initialBinding
C
By importing each other
D
Using Get.put inside views
Q3
of 3

What is the benefit of using `Get.lazyPut` inside module bindings?

A
It makes controllers permanent
B
It delays creation until the route is visited, improving startup time
C
It allows using tags
D
It automatically disposes controllers

Frequently Asked Questions

Can I share a module between multiple projects?

Yes, you can extract a module into its own package or copy it; it should be self‑contained if it uses only shared services.

How to handle deep linking with modules?

Define dynamic routes in GetPage (e.g., /product/:id). The binding will be called when that route is opened.

Can a module have multiple pages?

Absolutely. The product module can have both a list page and a detail page; both can share the same binding.

What about global dependencies like API clients?

Place them in app/shared/services and register them in initialBinding with permanent: true.

Does this structure work with GetX CLI?

Yes, the CLI’s get create page:name follows a similar modular structure.

Previous

getx rx workers advanced

Next

getx multi module project

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.