Skip to Content
  • Home
  • Book
  • Blog
  • About us
  • Help
  • Contact us
  •  [email protected]
  • Sign in
  • English (US) Español (BO)
  • Simplify It S.R.L.
    • Contact Us
Simplify It S.R.L.
      • Home
      • Book
      • Blog
      • About us
      • Help
      • Contact us
    •  [email protected]
    • English (US) Español (BO)
    • Sign in
    • Contact Us

    Chapter 13 Part 2: Advanced Service Patterns and Optimization

  • All Blogs
  • OWL Book
  • Chapter 13 Part 2: Advanced Service Patterns and Optimization
  • August 25, 2026 by
    Chapter 13 Part 2: Advanced Service Patterns and Optimization
    Grover Menacho

    Why this chapter? A single notification.add() call is easy; coordinating three services in one workflow, keeping a search box from hammering the server on every keystroke, and recovering from a flaky network call are the things that actually eat a workday once an addon grows past its first version. What is Odoo trying to solve with this? Odoo's UI has to stay responsive and resilient under real usage — spotty connections, slow endpoints, users who click twice — so the framework leans on patterns like debouncing, caching, and structured retries instead of leaving every developer to reinvent them. Real-world application: Debounced search, retry-with-backoff on a flaky external API call, and multi-step workflows spanning several services are patterns I reach for constantly once a client addon moves past its first proof-of-concept.

    In Part 1, we covered the services you'll use every day — notification, action, dialog, and a few essentials — along with the common mistakes junior developers run into with them. Now we'll tackle more demanding scenarios: composing several services into a single workflow, keeping expensive service calls fast, and recovering gracefully when a call fails.


    Advanced Service Patterns

    Pattern 3: Service Composition for Complex Workflows

    async processCustomerOrder(orderData) {
      const steps = [
        { name: "Validating order data", action: () => this.validateOrderData(orderData) },
        { name: "Creating customer", action: () => this.createCustomer(orderData.customer) },
        { name: "Creating order", action: () => this.createOrder(orderData) },
        { name: "Sending confirmation", action: () => this.sendConfirmationEmail(orderData) }
      ];
    
      let completedSteps = 0;
    
      try {
        // Initial progress notification
        this.notification.add(`Starting order processing (${steps.length} steps)...`, {
          type: "info"
        });
    
        for (const step of steps) {
          console.log(`Processing step: ${step.name}`);
    
          // Execute step
          await step.action();
          completedSteps++;
    
          // Progress notification
          this.notification.add(
            `${step.name} completed (${completedSteps}/${steps.length})`, 
            { type: "info" }
          );
        }
    
        // Success notification and navigation
        this.notification.add("Order processed successfully!", {
          type: "success",
          sticky: false
        });
    
        // Navigate to the order
        this.action.doAction({
          type: "ir.actions.act_window",
          res_model: "sale.order",
          res_id: orderData.orderId,
          views: [[false, "form"]],
          target: "current"
        });
    
      } catch (error) {
        console.error(`Order processing failed at step ${completedSteps + 1}:`, error);
    
        this.notification.add(
          `Order processing failed at step: ${steps[completedSteps]?.name || 'Unknown'}. ${error.message}`,
          { type: "danger", sticky: true }
        );
    
        // Optionally show rollback dialog
        this.dialog.add(ConfirmationDialog, {
          title: "Processing Failed",
          body: "Would you like to rollback the changes made so far?",
          confirm: () => this.rollbackChanges(completedSteps),
          cancel: () => console.log("User chose not to rollback")
        });
      }
    }
    

    Pattern 4: Service-Based State Management

    This pattern combines several services inside one component: useState for local reactive data, and orm/action/notification/dialog/user coordinated together. We'll build CustomerDashboard in three pieces.

    First, the constructor-like setup(): state, service references, and a small dataLoader object that groups the read-only queries the dashboard needs:

    export class CustomerDashboard extends Component {
      static template = "my_module.CustomerDashboard";
    
      setup() {
        this.state = useState({
          customers: [],
          selectedCustomer: null,
          loading: false,
          filters: {
            active: true,
            country: null,
            category: null
          }
        });
    
        // Initialize services
        this.orm = useService("orm");
        this.action = useService("action");
        this.notification = useService("notification");
        this.dialog = useService("dialog");
        this.user = useService("user");
    
        // Service-based data loader
        this.dataLoader = {
          customers: () => this.orm.searchRead(
            "res.partner",
            this.buildCustomerDomain(),
            ["name", "email", "phone", "country_id", "category_id"],
            { order: "name asc" }
          ),
    
          countries: () => this.orm.searchRead(
            "res.country",
            [],
            ["name", "code"],
            { order: "name asc" }
          ),
    
          categories: () => this.orm.searchRead(
            "res.partner.category",
            [],
            ["name", "color"],
            { order: "name asc" }
          )
        };
    
        // Initialize dashboard
        useEffect(
          () => {
            this.initializeDashboard();
          },
          () => []
        );
    
        // Reactive filter updates
        useEffect(
          () => {
            this.refreshCustomers();
          },
          () => [this.state.filters.active, this.state.filters.country, this.state.filters.category]
        );
      }
    

    Next, the method that runs once on mount, loading all three datasets in parallel and reporting success or failure:

      async initializeDashboard() {
        try {
          this.state.loading = true;
    
          // Load all required data in parallel
          const [customers, countries, categories] = await Promise.all([
            this.dataLoader.customers(),
            this.dataLoader.countries(),
            this.dataLoader.categories()
          ]);
    
          this.state.customers = customers;
          this.state.countries = countries;
          this.state.categories = categories;
    
          this.notification.add("Dashboard loaded successfully!", {
            type: "success"
          });
    
        } catch (error) {
          console.error("Dashboard initialization failed:", error);
    
          this.notification.add("Failed to load dashboard data.", {
            type: "danger",
            sticky: true
          });
        } finally {
          this.state.loading = false;
        }
      }
    

    Finally, the bulk-action methods: a lookup table of possible actions (archive, delete, export), a confirmation step for the dangerous ones, and the method that actually runs the chosen action:

      // Service-orchestrated actions
      async performBulkAction(action, customerIds) {
        const actionMap = {
          archive: {
            title: "Archive Customers",
            method: () => this.orm.write("res.partner", customerIds, { active: false }),
            successMessage: `${customerIds.length} customers archived successfully`
          },
    
          delete: {
            title: "Delete Customers",
            method: () => this.orm.unlink("res.partner", customerIds),
            successMessage: `${customerIds.length} customers deleted successfully`,
            dangerous: true
          },
    
          export: {
            title: "Export Customers",
            method: () => this.action.doAction({
              type: "ir.actions.report",
              report_name: "base.report_partner_list",
              context: { active_ids: customerIds }
            }),
            successMessage: "Customer export initiated"
          }
        };
    
        const actionConfig = actionMap[action];
        if (!actionConfig) return;
    
        // Show confirmation for dangerous actions
        if (actionConfig.dangerous) {
          this.dialog.add(ConfirmationDialog, {
            title: `Confirm ${actionConfig.title}`,
            body: `This will permanently ${action} ${customerIds.length} customers. This action cannot be undone.`,
            confirm: () => this.executeBulkAction(actionConfig)
          });
        } else {
          await this.executeBulkAction(actionConfig);
        }
      }
    
      async executeBulkAction(actionConfig) {
        try {
          this.notification.add(`${actionConfig.title} in progress...`, {
            type: "info"
          });
    
          await actionConfig.method();
    
          this.notification.add(actionConfig.successMessage, {
            type: "success"
          });
    
          // Refresh data after bulk action
          await this.refreshCustomers();
    
        } catch (error) {
          console.error(`${actionConfig.title} failed:`, error);
    
          this.notification.add(`${actionConfig.title} failed: ${error.message}`, {
            type: "danger",
            sticky: true
          });
        }
      }
    }
    

    Service Performance Optimization

    Debounced Service Calls

    Debouncing means delaying a function call until a burst of triggering events has stopped for a set amount of time — for example, waiting until the user pauses typing before firing a search request, instead of sending one request per keystroke. It's a common technique for keeping expensive service calls (like an ORM search) from overwhelming the server.

    setup() {
      this.orm = useService("orm");
    
      // Debounce expensive searches
      this.debouncedSearch = this.debounce(async (searchTerm) => {
        if (!searchTerm.trim()) return;
    
        try {
          const results = await this.orm.searchRead(
            "res.partner",
            [["name", "ilike", searchTerm]],
            ["name", "email"],
            { limit: 10 }
          );
    
          this.state.searchResults = results;
        } catch (error) {
          console.error("Search failed:", error);
        }
      }, 300); // Wait 300ms after user stops typing
    }
    
    debounce(func, wait) {
      let timeout;
      return (...args) => {
        clearTimeout(timeout);
        timeout = setTimeout(() => func.apply(this, args), wait);
      };
    }
    
    onSearchInput(event) {
      const searchTerm = event.target.value;
      this.state.searchTerm = searchTerm;
      this.debouncedSearch(searchTerm);
    }
    

    Odoo already ships a ready-made debounce helper, so in real addons you rarely need to write your own: import { debounce } from "@web/core/utils/timing";.

    Service Response Caching

    setup() {
      this.orm = useService("orm");
      this.cache = new Map();
      this.cacheExpiry = new Map();
    
      this.getCachedData = async (cacheKey, fetcher, ttl = 5 * 60 * 1000) => {
        // Check if we have valid cached data
        if (this.cache.has(cacheKey)) {
          const expiry = this.cacheExpiry.get(cacheKey);
          if (Date.now() < expiry) {
            console.log(`Using cached data for: ${cacheKey}`);
            return this.cache.get(cacheKey);
          }
        }
    
        // Fetch fresh data
        console.log(`Fetching fresh data for: ${cacheKey}`);
        const data = await fetcher();
    
        // Cache the result
        this.cache.set(cacheKey, data);
        this.cacheExpiry.set(cacheKey, Date.now() + ttl);
    
        return data;
      };
    }
    
    async loadCountries() {
      return this.getCachedData(
        'countries',
        () => this.orm.searchRead("res.country", [], ["name", "code"]),
        10 * 60 * 1000 // Cache for 10 minutes
      );
    }
    

    Service Error Recovery Strategies

    Automatic Retry with Exponential Backoff

    Exponential backoff means waiting longer between each retry attempt (1s, then 2s, then 4s, and so on) instead of retrying immediately — this gives a struggling server room to recover instead of hammering it with instant retries.

    async performResilientOperation(operation, maxRetries = 3) {
      let lastError;
    
      for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
          const result = await operation();
    
          if (attempt > 1) {
            this.notification.add("Operation succeeded after retry.", {
              type: "success"
            });
          }
    
          return result;
    
        } catch (error) {
          lastError = error;
          console.warn(`Attempt ${attempt} failed:`, error);
    
          if (attempt < maxRetries) {
            const delay = Math.pow(2, attempt - 1) * 1000; // Exponential backoff
    
            this.notification.add(`Operation failed, retrying in ${delay/1000}s... (${attempt}/${maxRetries})`, {
              type: "warning"
            });
    
            await new Promise(resolve => setTimeout(resolve, delay));
          }
        }
      }
    
      // All retries failed
      this.notification.add(`Operation failed after ${maxRetries} attempts: ${lastError.message}`, {
        type: "danger",
        sticky: true
      });
    
      throw lastError;
    }
    
    // Usage
    async saveData() {
      await this.performResilientOperation(async () => {
        return await this.orm.create("model.name", [this.state.formData]);
      });
    }
    

    Real-World Integration Example

    Here's a complete example showing how multiple services work together in a production scenario. We'll look at it in two pieces: the setup and data loading, then the action that processes selected orders.

    export class OrderProcessingCenter extends Component {
      static template = "my_module.OrderProcessingCenter";
    
      setup() {
        this.state = useState({
          orders: [],
          processing: false,
          selectedOrders: new Set(),
          statistics: {
            total: 0,
            pending: 0,
            processing: 0,
            completed: 0
          }
        });
    
        // Service initialization
        this.orm = useService("orm");
        this.action = useService("action");
        this.notification = useService("notification");
        this.dialog = useService("dialog");
        this.user = useService("user");
    
        // Load data on mount
        useEffect(
          () => {
            this.loadOrders();
            this.loadStatistics();
          },
          () => []
        );
      }
    
      async loadOrders() {
        try {
          const orders = await this.orm.searchRead(
            "sale.order",
            [["state", "in", ["draft", "sent", "sale"]]],
            ["name", "partner_id", "amount_total", "state", "date_order"],
            { order: "date_order desc", limit: 100 }
          );
    
          this.state.orders = orders;
        } catch (error) {
          this.notification.add("Failed to load orders.", { type: "danger" });
        }
      }
    
      async loadStatistics() {
        try {
          const stats = await this.orm.call("sale.order", "get_order_statistics");
          this.state.statistics = stats;
        } catch (error) {
          console.error("Failed to load statistics:", error);
        }
      }
    

    The interesting part is processSelectedOrders: it validates the selection, confirms with the user, then coordinates a batch ORM call, a success notification, a data refresh, and finally opens a report — all five services working together around one user action:

      async processSelectedOrders() {
        if (this.state.selectedOrders.size === 0) {
          this.notification.add("Please select orders to process.", {
            type: "warning"
          });
          return;
        }
    
        const orderIds = Array.from(this.state.selectedOrders);
    
        // Confirmation dialog
        this.dialog.add(ConfirmationDialog, {
          title: "Process Orders",
          body: `Process ${orderIds.length} selected orders?`,
          confirm: async () => {
            try {
              this.state.processing = true;
    
              // Progress notification
              this.notification.add(`Processing ${orderIds.length} orders...`, {
                type: "info"
              });
    
              // Batch process orders
              await this.orm.call("sale.order", "action_confirm", orderIds);
    
              // Success feedback
              this.notification.add(`Successfully processed ${orderIds.length} orders!`, {
                type: "success"
              });
    
              // Clear selection and refresh
              this.state.selectedOrders.clear();
              await this.loadOrders();
              await this.loadStatistics();
    
              // Open processing report
              this.action.doAction({
                type: "ir.actions.report",
                report_name: "sale.action_report_saleorder",
                context: { active_ids: orderIds }
              });
    
            } catch (error) {
              console.error("Order processing failed:", error);
    
              this.notification.add(`Processing failed: ${error.message}`, {
                type: "danger",
                sticky: true
              });
            } finally {
              this.state.processing = false;
            }
          }
        });
      }
    }
    

    Summary

    Odoo's built-in services provide a comprehensive toolkit for creating professional, integrated applications:

    • notification: User feedback and status updates
    • action: Navigation and Odoo integration
    • dialog: User interactions and confirmations
    • user, company, router: System information and navigation

    By mastering these services, your components will: - Feel native to the Odoo experience - Provide consistent user interactions - Handle errors gracefully - Integrate seamlessly with Odoo's workflows

    The key to success is understanding when and how to use each service, combining them effectively for complex workflows, and always prioritizing the user experience with clear feedback and intuitive interactions.

    In the next chapter, we'll explore advanced component patterns and architectural considerations for building large-scale OWL applications.

    TL;DR: Compose services deliberately (one notification per step of a multi-step workflow), debounce anything that fires on every keystroke with @web/core/utils/timing, and wrap unreliable calls in a retry-with-backoff strategy instead of failing silently.

    Try it yourself: This chapter's example is available as an installable Odoo 19 addon: simplifyit_owl_book_ch13_2_ex1. Installation instructions are in the repository README.


    Exercises

    1. Composed workflow: Write a method that creates a partner, then immediately creates a related contact for it, showing one progress notification per step (similar to Pattern 3). Make sure a failure at either step shows which step failed.
    2. Debounce it yourself: Implement a 300ms debounce over a live search input backed by orm.searchRead, without using Odoo's built-in debounce helper — then swap in import { debounce } from "@web/core/utils/timing" and confirm the behavior is the same.
    3. Retry with backoff: Take performResilientOperation and adapt it to retry an orm.call up to 3 times, then write a quick test (real or on paper) describing what notification the user should see after each failed attempt.

    What's Next?

    You've now covered the full service layer, from first notification to resilient multi-step workflows. Chapter 14 moves to composition — slots, scoped slots, and t-portal — the tools for building reusable, flexible components instead of one-off ones.

    in OWL Book
    Chapter 14: Advanced Composition with Slots
    Reading the whole book? Take it with you — free, no sign-up.
    PDF EPUB All chapters
    Useful Links
    • Home
    • About us
    • Blog
    • The OWL 2.0 book
    • Help
    • Contact us
    About us

    Simplify It S.R.L. is an Odoo implementation and development firm based in La Paz, Bolivia, working with companies across Latin America and North America. We have been building on Odoo since version 6.1: custom modules, version migrations and Bolivian localization.

    We are also the authors of the OWL 2.0 book, published free chapter by chapter on our blog.

    Connect with us
    • Contact us
    • [email protected]
    • +591 65144144
    • La Paz, Bolivia
    Follow us
    Copyright © Simplify It S.R.L.
    English (US) | Español (BO)
    Powered by Odoo - Create a free website