Maximizing the Benefits of BFF Pattern in API Design

Here, "API" refers to a Resource API that communicates directly with a database.

Introduction

As we developed our blog platform, the introduction of the BFF pattern allowed us to flexibly address business logic. However, we hadn't thoroughly considered the changes BFF would bring to API design. The question from a team member about what should be handled by the BFF and what should be offered by the API together made me realize the need for clear rules.

After introducing BFF, I believe that API endpoints must shift from being domain-centric to data-centric. This article will briefly introduce BFF and DDD before suggesting how API endpoints should be designed in the context of the BFF pattern.

What is BFF?

BFF (Backends For Frontends) emerged to provide backend services optimized for specific client requirements (web, mobile, etc.). In a microservices architecture, BFF can create new response values by combining responses from multiple APIs or by merging responses from various endpoints of a single API. This customizes data processing and optimizes network use for each client. Clients no longer communicate directly with the API but through a single instance of BFF. The API provides all information about a resource, while the BFF decides what information to expose through each endpoint.

What is DDD?

Domain-Driven Design (DDD) is a methodology that focuses on designing complex systems around the core concepts and rules of the business. This closely connects the development process with business needs, creating software that is flexible and easy to maintain. For instance, when implementing a 'Post' creation feature in a blog platform using Spring Boot and Kotlin, you can design a service layer to handle CRUD operations centered around the Blog domain model. This approach, informed by DDD, clearly implements business logic and creates a structure reusable across various interfaces (web, mobile).

Kotlin
import javax.persistence.* import java.time.LocalDateTime @Entity class Blog( @Id @GeneratedValue(strategy = GenerationType.AUTO) val id: Long? = null, var title: String, @OneToMany(mappedBy = "blog", cascade = arrayOf(CascadeType.ALL), fetch = FetchType.LAZY) val posts: MutableList<Post> = mutableListOf() ) @Entity class Post( @Id @GeneratedValue(strategy = GenerationType.AUTO) val id: Long? = null, var title: String, var content: String, var createdAt: LocalDateTime = LocalDateTime.now(), @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "blog_id") val blog: Blog ) @Repository interface BlogRepository : JpaRepository<Blog, Long> @Repository interface PostRepository : JpaRepository<Post, Long> @Service class BlogService( val blogRepository: BlogRepository, val postRepository: PostRepository ) { fun addPostToBlog(blogId: Long, post: Post): Post { val blog = blogRepository.findById(blogId).orElseThrow { Exception("Blog not found") } post.blog = blog blog.posts.add(post) postRepository.save(post) return post } }

Domain-Centric API

Domain-centric API endpoint design focuses on resources related to business logic, often adhering to RESTful principles. For example, an endpoint to retrieve all Posts for a specific Blog might be structured as GET /blog/{blogId}/post, revealing the parent-child relationship between Blog and Post. Conversely, a more generic endpoint to view posts across blogs would be GET /post?blog_id={blogId}, offering a flexible view of posts filtered by blog ID.

How Should API Change After Introducing BFF?

Post-BFF, API design should transition from domain-centric to data-centric. Domain-centric APIs can increase duplicate code in the process of redesigning for BFF and limit the freedom of BFF to define domains. Just as APIs mediate between the database and the user, BFF mediates between the API and the client, simplifying direct data provision roles.

Example

Using BFF in a blog platform, let's consider the listing of posting information. BFF redefines the Post domain by combining Post (title, thumbnail, summary) and Profile (blogger image, blogger name) from the API.

[sm]

BFF combines information by requesting GET /post for a list of posts and GET /profile?post_id={id1,id2,id3 …} based on post IDs to fetch profile information, thereby providing a combined response.

[sm]

Conclusion: Principles for Data-Centric API Endpoint Design

In the BFF pattern, APIs must delegate domain-centric thinking to the BFF, focusing instead on a data-centric approach. Here are suggested rules for designing API endpoints:

  1. 1

    Each endpoint should represent a single resource.

    Example: GET /{resource_name}

    Avoid representing parent-child relationships between resources. Only specify the name of the resource being requested at the top level.

  1. 2

    Think from a resource-centric perspective.

    A blog entity can have various states, such as being blocked if it's reported or deleted if the user removes it themselves. Endpoints should transparently create or modify resource information as it is.

    For instance, instead of using DELETE /blog/{blog_id} for removing a blog, use PATCH (PUT) /blog/{blog_id} to manage modifications, deletions, and blocks of blog information. BFF can provide more detailed endpoints for deletion.

  1. 3

    Endpoints should be transactional units.

    Let's say you're creating a user's post that includes elements like body, slug, description, and title. For search performance reasons, the post's body and other information are stored separately, in post and post_content tables, respectively.

    If saving to the post table fails, saving to the post_content must also fail, and vice versa. If the post table data is saved but post_content fails, it breaks data integrity. Thus, post and post_content belong to a single transaction. The API should offer only one endpoint to save or fetch both post and post_content together, not separate endpoints for each action.

Reference

  1. 2

    Is the REST API Okay as Is? [NAVER] - A summary of a LinkedIn presentation on DDD-centric APIs.