When would you choose spring-security-acl over a custom PermissionEvaluator or a query-level filter, and what are the pitfalls at scale?
answer
- ACL = dynamic, admin-managed, per-object sharing
- custom evaluator = static ownership/role rules
- collections → filter in the query, not @PostFilter
- post-filter breaks pagination + N+1
- method security = single-object backstop
basics
~20 sUse spring-security-acl only when permissions are per-object, admin-managed, and change at runtime (Google-Docs-style sharing). For ownership or role rules, a small custom PermissionEvaluator — or filtering in the SQL query — is simpler and faster.
solid answer
~50 sspring-security-acl shines when authorization is genuinely instance-level and dynamic: arbitrary users can be granted arbitrary permissions on individual objects at runtime, with inheritance and auditing — sharing features are the canonical case. Its cost is real: four tables, a row per grant, mandatory cache tuning, ACL lifecycle bootstrapping, and N+1 lookups when @PostFilter walks big collections. For the common cases — 'owner can edit', or role-scoped access — a hand-written PermissionEvaluator keyed on an ownership column is far leaner. And for list endpoints, the biggest pitfall is post-filtering: checking hasPermission per returned element after loading the whole page. Prefer pushing the predicate into the query (WHERE owner = :user or a join to the grant table) so the database returns only authorized rows, then use method security as a defense-in-depth backstop rather than the primary filter.
go deeper
Know ACLs are heavier and only for per-object, changeable permissions.
Contrast custom evaluator vs ACL and know post-filtering a big list is costly.
Articulate the N+1/pagination problem and push filtering into the query with method security as backstop.
Own the full trade-off: cost model, cache coherence, lifecycle, layering enforcement, and a consistent permission vocabulary across mechanisms.
## The decision framework Ask three questions: 1. **Is the rule instance-level?** If access is decided by role or a global flag, you don't need object security at all — `hasRole`/`hasAuthority` on the URL or method suffices. ACLs and PermissionEvaluators are only for 'which *instance*'. 2. **Is the rule static or dynamic?** If it's a fixed relationship ('the owner', 'members of the owning team'), a **custom `PermissionEvaluator`** computing that from your own columns is enough — no extra tables. If grants are **arbitrary and edited at runtime** by end users/admins (share doc 99 with Bob as READ, revoke tomorrow), you need permissions stored as *data* → **`spring-security-acl`**. 3. **How does it interact with listing/queries?** Object security via annotations checks **one object at a time**. Whole-collection endpoints are where designs fall apart. ## Why post-filtering is the classic scaling trap `@PostFilter("hasPermission(filterObject,'READ')")` runs the query, materializes the full page, then invokes the evaluator **per element**. With ACLs that's a lookup (cache or DB) per row → **N+1**, and worse, pagination breaks: you asked for 20 rows, the filter removes 7, the user sees 13 and the page counts are wrong. The correct pattern for large data sets is **filter in the data layer**: - Ownership: `WHERE owner_id = :currentUser`. - Shared access: `JOIN` against the grant/ACL table (`acl_entry` or your own) so the database returns only authorized rows *and* paginates correctly. Method-level `hasPermission` then stays as a **defense-in-depth backstop** on single-object reads/writes, not the primary list filter. ## Cost model of ACLs at scale - **Rows**: one `acl_object_identity` per secured object + one `acl_entry` per (sid, permission). Millions of objects × several grants each is a large, hot table. - **Cache**: `AclCache` is not optional at scale; you must size/evict it and keep it coherent when grants change (stale cache = wrong authz). - **Write amplification**: creating a domain object now also writes ACL rows in the same transaction; deletes must cascade or you leak orphan ACLs. - **Inheritance depth**: deep parent chains multiply `BasicLookupStrategy` work. ## When ACLs are the right call - Collaboration/sharing products (documents, folders, projects) where users grant each other fine-grained, revocable access. - Requirements for **per-object audit** of who-can-do-what and admin UIs to manage it. - Hierarchical resources that genuinely benefit from **ACL inheritance**. ## When to avoid them - Ownership-only or team-scoped access → ownership column + tiny custom evaluator + query filter. - Role/attribute rules → SpEL with `hasAuthority`, or attribute-based checks in a custom evaluator. - Very high-cardinality list endpoints → query-level filtering; ACLs as row source at most (join the grant table), never per-element post-filter. ## Architectural guidance Treat method security (`hasPermission`) as the **enforcement point for single-object operations** and a safety net; treat the **query** as the enforcement point for collections. Keep the *permission vocabulary* consistent (the same `'READ'/'WRITE'` semantics whether backed by ACLs, a custom evaluator, or SQL) so the mental model is uniform even if the mechanism varies per resource. Finally, remember `@PostAuthorize`/`@PostFilter` execute the method first — acceptable for reads, unacceptable where the method mutates or is expensive; prefer `@PreAuthorize` with the id-overload there.
- Why is @PostFilter over a large paginated list an anti-pattern with ACLs?It loads the whole page then evaluates hasPermission per row (N+1 ACL lookups), and removing rows post-query corrupts page sizes and counts. Push the predicate into the SQL (join the grant table) so the DB returns only authorized, correctly paginated rows.
- You need owner-only editing on one entity. ACLs or custom evaluator?Custom PermissionEvaluator (or even a direct check) reading the ownership column — no need for four ACL tables, caching, or ACL bootstrapping. Reserve ACLs for runtime-managed, arbitrary per-object sharing.
- How do you keep authorization correct when a grant is revoked but the AclCache still holds the old Acl?Evict/refresh the cached Acl on every ACL mutation (JdbcMutableAclService does this for its own writes); out-of-band changes to the tables must also invalidate the cache, or you get stale allow decisions.
saying these in an interview costs you the question
- Reaching for spring-security-acl for simple ownership or role rules.
- Using @PostFilter/hasPermission as the primary filter for large list endpoints and breaking pagination.
- Ignoring AclCache coherence, yielding stale allow/deny after grants change.
- Not creating/deleting ACL rows alongside the domain object lifecycle, leaving orphans or missing ACLs.
- Treating method security as the only enforcement layer for collections instead of filtering in the query.